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

gcivil-nyu-org / team1-wed-fall25 / 95

30 Oct 2025 04:04AM UTC coverage: 84.321% (+6.5%) from 77.84%
95

push

travis-pro

web-flow
Merge pull request #89 from gcivil-nyu-org/develop

Merge latest from develop into main

263 of 375 new or added lines in 11 files covered. (70.13%)

6 existing lines in 2 files now uncovered.

1366 of 1620 relevant lines covered (84.32%)

0.84 hits per line

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

53.44
/events/views.py
1
from django.shortcuts import render, redirect, get_object_or_404
1✔
2
from django.contrib.auth.decorators import login_required
1✔
3
from django.contrib import messages
1✔
4
from django.contrib.auth.models import User
1✔
5
from django.http import JsonResponse
1✔
6
from django.core.exceptions import ValidationError
1✔
7
from django.views.decorators.http import require_POST
1✔
8
from django.core.paginator import Paginator
1✔
9
from django.urls import reverse
1✔
10
from django.db import models
1✔
11

12
from .models import Event, EventInvite
1✔
13
from .forms import EventForm, parse_locations, parse_invites
1✔
14
from .services import (
1✔
15
    create_event,
16
    join_event as join_event_service,
17
    accept_invite as accept_invite_service,
18
    decline_invite as decline_invite_service,
19
)
20
from .selectors import (
1✔
21
    search_locations,
22
    search_users,
23
    public_event_pins,
24
    list_public_events,
25
    user_has_joined,
26
    list_user_invitations,
27
)
28
from .enums import InviteStatus, MessageReportReason
1✔
29
from .models import EventChatMessage, MessageReport, DirectChat, DirectMessage
1✔
30

31

32
@login_required
1✔
33
def create(request):
1✔
34
    """Create a new event"""
35
    if request.method == "POST":
1✔
36
        form = EventForm(request.POST)
1✔
37
        if form.is_valid():
1✔
38
            try:
1✔
39
                locations = parse_locations(request)
1✔
40
                invites = parse_invites(request)
1✔
41

42
                event = create_event(
1✔
43
                    host=request.user, form=form, locations=locations, invites=invites
44
                )
45

46
                messages.success(
1✔
47
                    request, f'Event "{event.title}" created successfully!'
48
                )
49
                return redirect(event.get_absolute_url())
1✔
50

51
            except ValueError as e:
×
52
                messages.error(request, str(e))
×
53
            except ValidationError as e:
×
54
                # Show the actual validation error message
55
                error_msg = e.message if hasattr(e, "message") else str(e)
×
56
                messages.error(request, error_msg)
×
57
            except Exception as e:
×
58
                # Show the specific error during development
59
                messages.error(request, f"An error occurred: {str(e)}")
×
60
    else:
61
        form = EventForm()
1✔
62

63
    return render(request, "events/create.html", {"form": form})
1✔
64

65

66
@login_required
1✔
67
def detail(request, slug):
1✔
68
    """
69
    Event detail page with dynamic content based on user role
70

71
    Context varies by role:
72
    - HOST/ATTENDEE: Full details + chat + members + join requests (host only)
73
    - VISITOR: Read-only details + join/request button
74
    """
75
    from .selectors import (
1✔
76
        get_event_detail,
77
        user_role_in_event,
78
        list_event_attendees,
79
        list_chat_messages,
80
        get_join_request,
81
        list_pending_join_requests,
82
    )
83
    from .models import EventFavorite
1✔
84

85
    try:
1✔
86
        event = get_event_detail(slug)
1✔
87
    except Event.DoesNotExist:
1✔
88
        messages.error(request, "Event not found.")
1✔
89
        return redirect("events:public")
1✔
90

91
    user_role = user_role_in_event(event, request.user)
1✔
92

93
    # Get additional locations (ordered stops)
94
    additional_locations = event.locations.all().order_by("order")
1✔
95

96
    # Get attendees list
97
    attendees = list_event_attendees(event)
1✔
98

99
    # Check if user has favorited this event
100
    is_favorited = EventFavorite.objects.filter(event=event, user=request.user).exists()
1✔
101

102
    context = {
1✔
103
        "event": event,
104
        "user_role": user_role,
105
        "additional_locations": additional_locations,
106
        "attendees": attendees,
107
        "is_favorited": is_favorited,
108
    }
109

110
    # Participant-specific data
111
    if user_role in ["HOST", "ATTENDEE"]:
1✔
112
        context["chat_messages"] = list_chat_messages(event, limit=20)
1✔
113

114
        # Host-specific data
115
        if user_role == "HOST":
1✔
116
            context["join_requests"] = list_pending_join_requests(event)
1✔
117

118
    # Visitor-specific data
119
    if user_role == "VISITOR":
1✔
120
        context["join_request"] = get_join_request(event, request.user)
×
121

122
    return render(request, "events/detail.html", context)
1✔
123

124

125
@login_required
1✔
126
def index(request):
1✔
127
    """Redirect to public events as default"""
128
    return redirect("events:public")
×
129

130

131
@login_required
1✔
132
def public_events(request):
1✔
133
    """Display public events with search, filter, and pagination"""
134
    from .models import EventFavorite
1✔
135

136
    # Parse query params
137
    query = request.GET.get("q", "").strip()
1✔
138
    visibility_filter = request.GET.get("filter", "")  # 'open' or 'invite'
1✔
139
    sort = request.GET.get("sort", "start_time")  # 'start_time' or '-start_time'
1✔
140

141
    # Get events
142
    events = list_public_events(
1✔
143
        query=query if query else None,
144
        visibility_filter=visibility_filter if visibility_filter else None,
145
        order=sort,
146
    )
147

148
    # Get user's favorited event IDs for efficient lookup
149
    favorited_ids = set(
1✔
150
        EventFavorite.objects.filter(user=request.user).values_list(
151
            "event_id", flat=True
152
        )
153
    )
154

155
    # Add 'joined' and 'is_favorited' attributes to each event
156
    events_list = []
1✔
157
    for event in events:
1✔
158
        event.joined = user_has_joined(event, request.user)
1✔
159
        event.is_favorited = event.id in favorited_ids
1✔
160
        events_list.append(event)
1✔
161

162
    # Pagination
163
    paginator = Paginator(events_list, 12)
1✔
164
    page_number = request.GET.get("page")
1✔
165
    page_obj = paginator.get_page(page_number)
1✔
166

167
    context = {
1✔
168
        "page_obj": page_obj,
169
        "query": query,
170
        "filter": visibility_filter,
171
        "sort": sort,
172
    }
173

174
    return render(request, "events/public_events.html", context)
1✔
175

176

177
@login_required
1✔
178
@require_POST
1✔
179
def join_event(request, slug):
1✔
180
    """Join a public event"""
181
    event = get_object_or_404(Event, slug=slug)
1✔
182

183
    try:
1✔
184
        join_event_service(event=event, user=request.user)
1✔
185
        messages.success(request, f'You have joined "{event.title}"!')
1✔
186
    except ValueError as e:
×
187
        messages.error(request, str(e))
×
188

189
    # Redirect back to public events with query params preserved
190
    redirect_url = reverse("events:public")
1✔
191
    query_params = request.GET.urlencode()
1✔
192
    if query_params:
1✔
193
        redirect_url += f"?{query_params}"
×
194

195
    return redirect(redirect_url)
1✔
196

197

198
@login_required
1✔
199
def invitations(request):
1✔
200
    """Display pending invitations for current user"""
201
    invites = list_user_invitations(request.user)
×
202

203
    return render(request, "events/invitations.html", {"invites": invites})
×
204

205

206
@login_required
1✔
207
@require_POST
1✔
208
def accept_invite(request, slug):
1✔
209
    """Accept an invitation"""
210
    event = get_object_or_404(Event, slug=slug)
×
211
    invite = get_object_or_404(
×
212
        EventInvite, event=event, invitee=request.user, status=InviteStatus.PENDING
213
    )
214

215
    try:
×
216
        accept_invite_service(invite=invite)
×
217
        messages.success(
×
218
            request, f'You have accepted the invitation to "{event.title}"!'
219
        )
220
        return redirect(event.get_absolute_url())
×
221
    except Exception:
×
222
        messages.error(request, "Failed to accept invitation.")
×
223
        return redirect("events:invitations")
×
224

225

226
@login_required
1✔
227
@require_POST
1✔
228
def decline_invite(request, slug):
1✔
229
    """Decline an invitation"""
230
    event = get_object_or_404(Event, slug=slug)
×
231
    invite = get_object_or_404(
×
232
        EventInvite, event=event, invitee=request.user, status=InviteStatus.PENDING
233
    )
234

235
    try:
×
236
        decline_invite_service(invite=invite)
×
237
        messages.success(request, "Invitation declined.")
×
238
    except Exception:
×
239
        messages.error(request, "Failed to decline invitation.")
×
240

241
    return redirect("events:invitations")
×
242

243

244
@login_required
1✔
245
def api_locations_search(request):
1✔
246
    """JSON API for location autocomplete"""
247
    term = request.GET.get("q", "").strip()
1✔
248

249
    if not term or len(term) < 2:
1✔
250
        return JsonResponse({"results": []})
×
251

252
    locations = search_locations(term, limit=10)
1✔
253

254
    # Return compact format matching map style
255
    results = [
1✔
256
        {
257
            "id": loc["id"],
258
            "t": loc["title"] or "Untitled",
259
            "a": loc["artist_name"] or "Unknown",
260
            "y": float(loc["latitude"]),
261
            "x": float(loc["longitude"]),
262
        }
263
        for loc in locations
264
    ]
265

266
    return JsonResponse({"results": results})
1✔
267

268

269
@login_required
1✔
270
def api_users_search(request):
1✔
271
    """JSON API for user autocomplete"""
272
    term = request.GET.get("q", "").strip()
1✔
273

274
    if not term or len(term) < 2:
1✔
275
        return JsonResponse({"results": []})
×
276

277
    users = search_users(term, limit=10)
1✔
278

279
    results = [{"id": user["id"], "u": user["username"]} for user in users]
1✔
280

281
    return JsonResponse({"results": results})
1✔
282

283

284
@login_required
1✔
285
def api_event_pins(request):
1✔
286
    """JSON API for event pins on map"""
287
    events = public_event_pins()
1✔
288

289
    points = [
1✔
290
        {
291
            "id": event["id"],
292
            "t": event["title"],
293
            "y": float(event["start_location__latitude"]),
294
            "x": float(event["start_location__longitude"]),
295
            "slug": event["slug"],
296
        }
297
        for event in events
298
    ]
299

300
    return JsonResponse({"points": points})
1✔
301

302

303
@login_required
1✔
304
def api_chat_messages(request, slug):
1✔
305
    """JSON API for real-time chat messages"""
NEW
306
    from .selectors import list_chat_messages, user_role_in_event
×
307

NEW
308
    event = get_object_or_404(Event, slug=slug)
×
309

310
    # Check if user is a member
NEW
311
    role = user_role_in_event(event, request.user)
×
NEW
312
    if role == "VISITOR":
×
NEW
313
        return JsonResponse({"error": "Only event members can view chat"}, status=403)
×
314

315
    # Get messages
NEW
316
    messages = list_chat_messages(event, limit=20)
×
317

318
    # Format for JSON response
NEW
319
    messages_data = [
×
320
        {
321
            "id": msg.id,
322
            "author": msg.author.username,
323
            "is_host": msg.author == event.host,
324
            "message": msg.message,
325
            "created_at": msg.created_at.strftime("%b %d, %I:%M %p"),
326
        }
327
        for msg in messages
328
    ]
329

NEW
330
    return JsonResponse({"messages": messages_data})
×
331

332

333
# PHASE 3 VIEWS
334

335

336
@login_required
1✔
337
@require_POST
1✔
338
def chat_send(request, slug):
1✔
339
    """Send a chat message"""
340
    from .services import post_chat_message as post_chat_service
×
341

342
    event = get_object_or_404(Event, slug=slug)
×
343
    message = request.POST.get("message", "").strip()
×
344

345
    try:
×
346
        post_chat_service(event=event, user=request.user, message=message)
×
347
        messages.success(request, "Message sent!")
×
348
    except ValueError as e:
×
349
        messages.error(request, str(e))
×
350

351
    return redirect(event.get_absolute_url() + "#chat")
×
352

353

354
@login_required
1✔
355
@require_POST
1✔
356
def request_join_view(request, slug):
1✔
357
    """Request to join a PUBLIC_INVITE event"""
358
    from .services import request_join as request_join_service
×
359

360
    event = get_object_or_404(Event, slug=slug)
×
361

362
    try:
×
363
        request_join_service(event=event, user=request.user)
×
364
        messages.success(request, "Join request sent to host!")
×
365
    except ValueError as e:
×
366
        messages.error(request, str(e))
×
367

368
    return redirect(event.get_absolute_url())
×
369

370

371
@login_required
1✔
372
@require_POST
1✔
373
def approve_request(request, slug, request_id):
1✔
374
    """Host approves a join request"""
375
    from .services import approve_join_request as approve_service
×
376
    from .models import EventJoinRequest
×
377

378
    event = get_object_or_404(Event, slug=slug)
×
379

380
    # Verify user is host
381
    if event.host != request.user:
×
382
        messages.error(request, "Only the host can manage join requests.")
×
383
        return redirect(event.get_absolute_url())
×
384

385
    join_request = get_object_or_404(EventJoinRequest, id=request_id, event=event)
×
386

387
    try:
×
388
        approve_service(join_request=join_request)
×
389
        messages.success(
×
390
            request, f"{join_request.requester.username} has been added to the event!"
391
        )
392
    except Exception:
×
393
        messages.error(request, "Failed to approve request.")
×
394

395
    return redirect(event.get_absolute_url())
×
396

397

398
@login_required
1✔
399
@require_POST
1✔
400
def decline_request(request, slug, request_id):
1✔
401
    """Host declines a join request"""
402
    from .services import decline_join_request as decline_service
×
403
    from .models import EventJoinRequest
×
404

405
    event = get_object_or_404(Event, slug=slug)
×
406

407
    # Verify user is host
408
    if event.host != request.user:
×
409
        messages.error(request, "Only the host can manage join requests.")
×
410
        return redirect(event.get_absolute_url())
×
411

412
    join_request = get_object_or_404(EventJoinRequest, id=request_id, event=event)
×
413

414
    try:
×
415
        decline_service(join_request=join_request)
×
416
        messages.success(request, "Join request declined.")
×
417
    except Exception:
×
418
        messages.error(request, "Failed to decline request.")
×
419

420
    return redirect(event.get_absolute_url())
×
421

422

423
@login_required
1✔
424
def update_event(request, slug):
1✔
425
    """Update an existing event (host only)"""
426
    from .services import update_event as update_event_service
1✔
427

428
    event = get_object_or_404(Event, slug=slug, is_deleted=False)
1✔
429

430
    # Verify user is host
431
    if event.host != request.user:
1✔
432
        messages.error(request, "Only the host can edit this event.")
1✔
433
        return redirect(event.get_absolute_url())
1✔
434

435
    if request.method == "POST":
1✔
436
        form = EventForm(request.POST, instance=event)
×
437
        if form.is_valid():
×
438
            try:
×
439
                locations = parse_locations(request)
×
440
                invites = parse_invites(request)
×
441

442
                updated_event = update_event_service(
×
443
                    event=event, form=form, locations=locations, invites=invites
444
                )
445

446
                messages.success(request, f'Event "{updated_event.title}" updated!')
×
447
                return redirect(updated_event.get_absolute_url())
×
448

449
            except ValueError as e:
×
450
                messages.error(request, str(e))
×
451
            except ValidationError as e:
×
452
                error_msg = e.message if hasattr(e, "message") else str(e)
×
453
                messages.error(request, error_msg)
×
454
            except Exception as e:
×
455
                messages.error(request, f"An error occurred: {str(e)}")
×
456
    else:
457
        # Pre-populate form with existing data
458
        form = EventForm(instance=event)
1✔
459

460
    # Get existing locations and invites for pre-population
461
    existing_locations = list(
1✔
462
        event.locations.all().order_by("order").values_list("location_id", flat=True)
463
    )
464
    existing_invites = list(
1✔
465
        EventInvite.objects.filter(event=event).values_list("invitee_id", flat=True)
466
    )
467

468
    context = {
1✔
469
        "form": form,
470
        "event": event,
471
        "existing_locations": existing_locations,
472
        "existing_invites": existing_invites,
473
        "is_update": True,
474
    }
475

476
    return render(request, "events/create.html", context)
1✔
477

478

479
@login_required
1✔
480
@require_POST
1✔
481
def delete_event(request, slug):
1✔
482
    """Delete an event (host only)"""
483
    from .services import delete_event as delete_event_service
1✔
484

485
    event = get_object_or_404(Event, slug=slug, is_deleted=False)
1✔
486

487
    # Verify user is host
488
    if event.host != request.user:
1✔
489
        messages.error(request, "Only the host can delete this event.")
1✔
490
        return redirect(event.get_absolute_url())
1✔
491

492
    try:
1✔
493
        delete_event_service(event=event)
1✔
494
        messages.success(request, f'Event "{event.title}" has been deleted.')
1✔
495
        return redirect("events:public")
1✔
496
    except Exception as e:
×
497
        messages.error(request, f"Failed to delete event: {str(e)}")
×
498
        return redirect(event.get_absolute_url())
×
499

500

501
@login_required
1✔
502
@require_POST
1✔
503
def leave_event(request, slug):
1✔
504
    """Leave an event (attendee deregistration)"""
505
    from .services import leave_event as leave_event_service
1✔
506

507
    event = get_object_or_404(Event, slug=slug, is_deleted=False)
1✔
508

509
    try:
1✔
510
        leave_event_service(event=event, user=request.user)
1✔
511
        messages.success(request, f'You have left "{event.title}".')
1✔
512
        return redirect("events:public")
1✔
513
    except ValueError as e:
1✔
514
        messages.error(request, str(e))
1✔
515
        return redirect(event.get_absolute_url())
1✔
516

517

518
@login_required
1✔
519
@require_POST
1✔
520
def favorite_event_view(request, slug):
1✔
521
    """Add event to favorites"""
522
    from .services import favorite_event as favorite_event_service
1✔
523

524
    event = get_object_or_404(Event, slug=slug, is_deleted=False)
1✔
525

526
    try:
1✔
527
        favorite_event_service(event=event, user=request.user)
1✔
528
        messages.success(request, f'Added "{event.title}" to favorites!')
1✔
529
    except ValueError as e:
×
530
        messages.error(request, str(e))
×
531

532
    # Redirect back to the referring page or event detail
533
    return redirect(request.META.get("HTTP_REFERER", event.get_absolute_url()))
1✔
534

535

536
@login_required
1✔
537
@require_POST
1✔
538
def unfavorite_event_view(request, slug):
1✔
539
    """Remove event from favorites"""
540
    from .services import unfavorite_event as unfavorite_event_service
1✔
541

542
    event = get_object_or_404(Event, slug=slug)
1✔
543

544
    try:
1✔
545
        unfavorite_event_service(event=event, user=request.user)
1✔
546
        messages.success(request, f'Removed "{event.title}" from favorites.')
1✔
547
    except Exception as e:
×
548
        messages.error(request, f"Failed to remove from favorites: {str(e)}")
×
549

550
    # Redirect back to the referring page or event detail
551
    return redirect(request.META.get("HTTP_REFERER", event.get_absolute_url()))
1✔
552

553

554
@login_required
1✔
555
def favorites(request):
1✔
556
    """Display user's favorite events"""
557
    from .models import EventFavorite
1✔
558

559
    # Get favorited events (excluding deleted ones)
560
    favorites_qs = (
1✔
561
        EventFavorite.objects.filter(user=request.user, event__is_deleted=False)
562
        .select_related("event", "event__host", "event__start_location")
563
        .order_by("-created_at")
564
    )
565

566
    # Add 'joined' attribute to each event
567
    favorites_list = []
1✔
568
    for fav in favorites_qs:
1✔
569
        fav.event.joined = user_has_joined(fav.event, request.user)
×
570
        fav.event.favorited_at = fav.created_at
×
571
        favorites_list.append(fav.event)
×
572

573
    # Pagination
574
    paginator = Paginator(favorites_list, 12)
1✔
575
    page_number = request.GET.get("page")
1✔
576
    page_obj = paginator.get_page(page_number)
1✔
577

578
    context = {
1✔
579
        "page_obj": page_obj,
580
    }
581

582
    return render(request, "events/favorites.html", context)
1✔
583

584

585
@login_required
1✔
586
@require_POST
1✔
587
def report_message(request, message_id):
1✔
588
    """Report an inappropriate chat message"""
NEW
589
    from .enums import ReportStatus
×
590

NEW
591
    message = get_object_or_404(EventChatMessage, id=message_id)
×
592

593
    # Get reason and description from POST data
NEW
594
    reason = request.POST.get("reason")
×
NEW
595
    description = request.POST.get("description", "").strip()
×
596

597
    # Validate reason
NEW
598
    choices = [choice[0] for choice in MessageReportReason.choices]
×
NEW
599
    if not reason or reason not in choices:
×
NEW
600
        return JsonResponse({"error": "Invalid reason"}, status=400)
×
601

602
    # Check if user already reported this message
NEW
603
    existing_report = MessageReport.objects.filter(
×
604
        message=message, reporter=request.user
605
    ).exists()
606

NEW
607
    if existing_report:
×
NEW
608
        error_msg = "You have already reported this message"
×
NEW
609
        return JsonResponse({"error": error_msg}, status=400)
×
610

611
    # Create report
NEW
612
    MessageReport.objects.create(
×
613
        message=message,
614
        reporter=request.user,
615
        reason=reason,
616
        description=description,
617
        status=ReportStatus.PENDING,
618
    )
619

NEW
620
    return JsonResponse({"success": True, "message": "Message reported successfully"})
×
621

622

623
@login_required
1✔
624
@require_POST
1✔
625
def create_or_get_direct_chat(request, slug):
1✔
626
    """Create or get existing direct chat with another user in event"""
NEW
627
    event = get_object_or_404(Event, slug=slug, is_deleted=False)
×
NEW
628
    other_user_id = request.POST.get("other_user_id")
×
629

NEW
630
    try:
×
NEW
631
        other_user = User.objects.get(id=other_user_id)
×
NEW
632
    except User.DoesNotExist:
×
NEW
633
        return JsonResponse({"error": "User not found"}, status=404)
×
634

635
    # Check both users are event members
NEW
636
    from .selectors import user_role_in_event
×
637

NEW
638
    requester_role = user_role_in_event(event, request.user)
×
NEW
639
    other_role = user_role_in_event(event, other_user)
×
640

NEW
641
    if requester_role == "VISITOR" or other_role == "VISITOR":
×
NEW
642
        return JsonResponse({"error": "Both users must be event members"}, status=403)
×
643

644
    # Create or get chat (ensure user1 < user2 for uniqueness)
NEW
645
    user1, user2 = (
×
646
        (request.user, other_user)
647
        if request.user.id < other_user.id
648
        else (other_user, request.user)
649
    )
650

NEW
651
    chat, created = DirectChat.objects.get_or_create(
×
652
        event=event,
653
        user1=user1,
654
        user2=user2,
655
    )
656

NEW
657
    return JsonResponse({"chat_id": chat.id})
×
658

659

660
@login_required
1✔
661
@require_POST
1✔
662
def send_direct_message(request, chat_id):
1✔
663
    """Send message in direct chat"""
NEW
664
    chat = get_object_or_404(DirectChat, id=chat_id)
×
665

666
    # Verify user is part of this chat
NEW
667
    if request.user not in [chat.user1, chat.user2]:
×
NEW
668
        return JsonResponse({"error": "Not authorized"}, status=403)
×
669

NEW
670
    content = request.POST.get("content", "").strip()
×
NEW
671
    if not content:
×
NEW
672
        return JsonResponse({"error": "Message cannot be empty"}, status=400)
×
673

NEW
674
    if len(content) > 500:
×
NEW
675
        return JsonResponse({"error": "Message too long"}, status=400)
×
676

NEW
677
    message = DirectMessage.objects.create(
×
678
        chat=chat, sender=request.user, content=content
679
    )
680

681
    # Auto-restore chat for recipient if they had left
NEW
682
    from .models import DirectChatLeave
×
683

NEW
684
    other_user = chat.get_other_user(request.user)
×
685

686
    # If the other user had left this chat, restore them
NEW
687
    DirectChatLeave.objects.filter(chat=chat, user=other_user).delete()
×
688

689
    # Update chat timestamp
NEW
690
    chat.save()
×
691

NEW
692
    return JsonResponse({"success": True, "message_id": message.id})
×
693

694

695
@login_required
1✔
696
def api_direct_messages(request, chat_id):
1✔
697
    """Get messages for a direct chat"""
NEW
698
    chat = get_object_or_404(DirectChat, id=chat_id)
×
699

700
    # Verify user is part of this chat
NEW
701
    if request.user not in [chat.user1, chat.user2]:
×
NEW
702
        return JsonResponse({"error": "Not authorized"}, status=403)
×
703

NEW
704
    messages = DirectMessage.objects.filter(chat=chat).order_by("created_at")
×
705

NEW
706
    messages_data = [
×
707
        {
708
            "id": msg.id,
709
            "sender": msg.sender.username,
710
            "content": msg.content,
711
            "created_at": msg.created_at.strftime("%b %d, %I:%M %p"),
712
            "is_own": msg.sender == request.user,
713
        }
714
        for msg in messages
715
    ]
716

717
    # Mark messages as read
NEW
718
    DirectMessage.objects.filter(chat=chat, is_read=False).exclude(
×
719
        sender=request.user
720
    ).update(is_read=True)
721

NEW
722
    return JsonResponse({"messages": messages_data})
×
723

724

725
@login_required
1✔
726
def list_user_direct_chats(request):
1✔
727
    """Get list of all direct chats for the current user"""
728
    # Get all chats where user is either user1 or user2 and hasn't left
729

NEW
730
    chats = (
×
731
        DirectChat.objects.filter(
732
            models.Q(user1=request.user) | models.Q(user2=request.user)
733
        )
734
        .exclude(models.Q(leaves__user=request.user))
735
        .select_related("event", "user1", "user2")
736
        .order_by("-updated_at")
737
    )
738

NEW
739
    chats_data = []
×
NEW
740
    for chat in chats:
×
NEW
741
        other_user = chat.get_other_user(request.user)
×
742
        # Get latest message
NEW
743
        latest_message = (
×
744
            DirectMessage.objects.filter(chat=chat).order_by("-created_at").first()
745
        )
746
        # Count unread messages
NEW
747
        unread_count = (
×
748
            DirectMessage.objects.filter(chat=chat, is_read=False)
749
            .exclude(sender=request.user)
750
            .count()
751
        )
752

NEW
753
        chats_data.append(
×
754
            {
755
                "chat_id": chat.id,
756
                "event_title": chat.event.title,
757
                "event_slug": chat.event.slug,
758
                "other_user": other_user.username,
759
                "other_user_id": other_user.id,
760
                "latest_message": latest_message.content if latest_message else "",
761
                "latest_time": (
762
                    latest_message.created_at.strftime("%b %d, %I:%M %p")
763
                    if latest_message
764
                    else ""
765
                ),
766
                "unread_count": unread_count,
767
            }
768
        )
769

NEW
770
    return JsonResponse({"chats": chats_data})
×
771

772

773
@login_required
1✔
774
def delete_direct_chat(request, chat_id):
1✔
775
    """Leave a direct chat (don't delete the chat itself)"""
NEW
776
    try:
×
NEW
777
        chat = DirectChat.objects.get(
×
778
            models.Q(user1=request.user) | models.Q(user2=request.user), id=chat_id
779
        )
780

781
        # Check if user has already left
NEW
782
        if chat.has_user_left(request.user):
×
NEW
783
            return JsonResponse(
×
784
                {"error": "You have already left this chat"}, status=400
785
            )
786

787
        # Create leave record instead of deleting
NEW
788
        from .models import DirectChatLeave
×
789

NEW
790
        DirectChatLeave.objects.get_or_create(chat=chat, user=request.user)
×
791

NEW
792
        return JsonResponse({"success": True})
×
793

NEW
794
    except DirectChat.DoesNotExist:
×
NEW
795
        return JsonResponse({"error": "Chat not found"}, status=404)
×
NEW
796
    except Exception as e:
×
NEW
797
        return JsonResponse({"error": str(e)}, status=500)
×
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