• 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

98.04
/events/models.py
1
import uuid
1✔
2
from django.db import models
1✔
3
from django.contrib.auth.models import User
1✔
4
from django.urls import reverse
1✔
5
from django.utils.text import slugify
1✔
6

7
from loc_detail.models import PublicArt
1✔
8
from .enums import (
1✔
9
    EventVisibility,
10
    MembershipRole,
11
    InviteStatus,
12
    JoinRequestStatus,
13
    MessageReportReason,
14
    ReportStatus,
15
)
16

17

18
class Event(models.Model):
1✔
19
    """Core event model"""
20

21
    slug = models.SlugField(unique=True, max_length=100, db_index=True)
1✔
22
    title = models.CharField(max_length=80)
1✔
23
    host = models.ForeignKey(
1✔
24
        User, on_delete=models.CASCADE, related_name="hosted_events"
25
    )
26
    visibility = models.CharField(
1✔
27
        max_length=20,
28
        choices=EventVisibility.choices,
29
        default=EventVisibility.PUBLIC_OPEN,
30
    )
31
    start_time = models.DateTimeField(db_index=True)
1✔
32
    start_location = models.ForeignKey(
1✔
33
        PublicArt, on_delete=models.PROTECT, related_name="events"
34
    )
35
    description = models.TextField(blank=True)
1✔
36
    is_deleted = models.BooleanField(default=False)
1✔
37
    created_at = models.DateTimeField(auto_now_add=True)
1✔
38
    updated_at = models.DateTimeField(auto_now=True)
1✔
39

40
    class Meta:
1✔
41
        indexes = [
1✔
42
            models.Index(fields=["slug"]),
43
            models.Index(fields=["start_time"]),
44
            models.Index(fields=["visibility"]),
45
            models.Index(fields=["host", "start_time"]),
46
        ]
47
        ordering = ["-start_time"]
1✔
48

49
    def __str__(self):
1✔
50
        return f"{self.title} by {self.host.username}"
1✔
51

52
    def save(self, *args, **kwargs):
1✔
53
        if not self.slug:
1✔
54
            base_slug = slugify(self.title)[:50]
1✔
55
            unique_id = str(uuid.uuid4())[:8]
1✔
56
            self.slug = f"{base_slug}-{unique_id}"
1✔
57
        super().save(*args, **kwargs)
1✔
58

59
    def get_absolute_url(self):
1✔
60
        return reverse("events:detail", kwargs={"slug": self.slug})
1✔
61

62

63
class EventLocation(models.Model):
1✔
64
    """Ordered itinerary stops beyond the starting location"""
65

66
    event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="locations")
1✔
67
    location = models.ForeignKey(PublicArt, on_delete=models.PROTECT)
1✔
68
    order = models.PositiveSmallIntegerField()
1✔
69
    note = models.CharField(max_length=100, blank=True)
1✔
70

71
    class Meta:
1✔
72
        constraints = [
1✔
73
            models.UniqueConstraint(
74
                fields=["event", "order"], name="uniq_event_location_order"
75
            ),
76
            models.UniqueConstraint(
77
                fields=["event", "location"], name="uniq_event_location_pair"
78
            ),
79
        ]
80
        ordering = ["order"]
1✔
81

82
    def __str__(self):
1✔
83
        return f"{self.event.title} - Stop {self.order}"
1✔
84

85

86
class EventMembership(models.Model):
1✔
87
    """Tracks who is in the event and their role"""
88

89
    event = models.ForeignKey(
1✔
90
        Event, on_delete=models.CASCADE, related_name="memberships"
91
    )
92
    user = models.ForeignKey(
1✔
93
        User, on_delete=models.CASCADE, related_name="event_memberships"
94
    )
95
    role = models.CharField(max_length=20, choices=MembershipRole.choices)
1✔
96
    joined_at = models.DateTimeField(auto_now_add=True)
1✔
97

98
    class Meta:
1✔
99
        constraints = [
1✔
100
            models.UniqueConstraint(
101
                fields=["event", "user"], name="uniq_event_user_membership"
102
            )
103
        ]
104
        indexes = [
1✔
105
            models.Index(fields=["event", "role"]),
106
            models.Index(fields=["user"]),
107
        ]
108

109
    def __str__(self):
1✔
110
        return f"{self.user.username} - {self.role} at {self.event.title}"
1✔
111

112

113
class EventInvite(models.Model):
1✔
114
    """Tracks invite lifecycle"""
115

116
    event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="invites")
1✔
117
    invited_by = models.ForeignKey(
1✔
118
        User, on_delete=models.SET_NULL, null=True, related_name="sent_invites"
119
    )
120
    invitee = models.ForeignKey(
1✔
121
        User, on_delete=models.CASCADE, related_name="event_invitations"
122
    )
123
    status = models.CharField(
1✔
124
        max_length=20, choices=InviteStatus.choices, default=InviteStatus.PENDING
125
    )
126
    created_at = models.DateTimeField(auto_now_add=True)
1✔
127
    responded_at = models.DateTimeField(null=True, blank=True)
1✔
128

129
    class Meta:
1✔
130
        constraints = [
1✔
131
            models.UniqueConstraint(
132
                fields=["event", "invitee"], name="uniq_event_invitee"
133
            )
134
        ]
135
        indexes = [
1✔
136
            models.Index(fields=["invitee", "status"]),
137
            models.Index(fields=["event"]),
138
        ]
139

140
    def __str__(self):
1✔
141
        return f"Invite to {self.invitee.username} for {self.event.title}"
1✔
142

143

144
class EventChatMessage(models.Model):
1✔
145
    """Chat messages for event attendees (Phase 3)"""
146

147
    event = models.ForeignKey(
1✔
148
        Event, on_delete=models.CASCADE, related_name="chat_messages"
149
    )
150
    author = models.ForeignKey(User, on_delete=models.CASCADE)
1✔
151
    message = models.CharField(max_length=300)
1✔
152
    created_at = models.DateTimeField(auto_now_add=True, db_index=True)
1✔
153

154
    class Meta:
1✔
155
        indexes = [
1✔
156
            models.Index(fields=["event", "-created_at"]),
157
        ]
158
        ordering = ["created_at"]
1✔
159

160
    def __str__(self):
1✔
161
        return f"{self.author.username}: {self.message[:50]}"
1✔
162

163

164
class EventJoinRequest(models.Model):
1✔
165
    """Visitors requesting to join PUBLIC_INVITE events (Phase 3)"""
166

167
    event = models.ForeignKey(
1✔
168
        Event, on_delete=models.CASCADE, related_name="join_requests"
169
    )
170
    requester = models.ForeignKey(User, on_delete=models.CASCADE)
1✔
171
    status = models.CharField(
1✔
172
        max_length=20,
173
        choices=JoinRequestStatus.choices,
174
        default=JoinRequestStatus.PENDING,
175
    )
176
    created_at = models.DateTimeField(auto_now_add=True)
1✔
177
    decided_at = models.DateTimeField(null=True, blank=True)
1✔
178

179
    class Meta:
1✔
180
        constraints = [
1✔
181
            models.UniqueConstraint(
182
                fields=["event", "requester"], name="uniq_event_join_request"
183
            )
184
        ]
185
        indexes = [
1✔
186
            models.Index(fields=["event", "status"]),
187
            models.Index(fields=["requester"]),
188
        ]
189

190
    def __str__(self):
1✔
191
        return f"Join request by {self.requester.username} for {self.event.title}"
1✔
192

193

194
class EventFavorite(models.Model):
1✔
195
    """Tracks user's favorited events"""
196

197
    event = models.ForeignKey(
1✔
198
        Event, on_delete=models.CASCADE, related_name="favorited_by"
199
    )
200
    user = models.ForeignKey(
1✔
201
        User, on_delete=models.CASCADE, related_name="favorite_events"
202
    )
203
    created_at = models.DateTimeField(auto_now_add=True, db_index=True)
1✔
204

205
    class Meta:
1✔
206
        constraints = [
1✔
207
            models.UniqueConstraint(
208
                fields=["event", "user"], name="uniq_event_favorite"
209
            )
210
        ]
211
        indexes = [
1✔
212
            models.Index(fields=["user", "-created_at"]),
213
            models.Index(fields=["event"]),
214
        ]
215
        ordering = ["-created_at"]
1✔
216

217
    def __str__(self):
1✔
218
        return f"{self.user.username} favorited {self.event.title}"
1✔
219

220

221
class MessageReport(models.Model):
1✔
222
    """Reports for inappropriate chat messages"""
223

224
    message = models.ForeignKey(
1✔
225
        EventChatMessage, on_delete=models.CASCADE, related_name="reports"
226
    )
227
    reporter = models.ForeignKey(
1✔
228
        User, on_delete=models.CASCADE, related_name="message_reports"
229
    )
230
    reason = models.CharField(max_length=20, choices=MessageReportReason.choices)
1✔
231
    description = models.TextField(blank=True, max_length=500)
1✔
232
    status = models.CharField(
1✔
233
        max_length=20, choices=ReportStatus.choices, default=ReportStatus.PENDING
234
    )
235
    reviewed_by = models.ForeignKey(
1✔
236
        User,
237
        on_delete=models.SET_NULL,
238
        null=True,
239
        blank=True,
240
        related_name="reviewed_message_reports",
241
    )
242
    created_at = models.DateTimeField(auto_now_add=True, db_index=True)
1✔
243
    reviewed_at = models.DateTimeField(null=True, blank=True)
1✔
244

245
    class Meta:
1✔
246
        constraints = [
1✔
247
            models.UniqueConstraint(
248
                fields=["message", "reporter"], name="uniq_message_report"
249
            )
250
        ]
251
        indexes = [
1✔
252
            models.Index(fields=["status", "-created_at"]),
253
            models.Index(fields=["message"]),
254
            models.Index(fields=["reporter"]),
255
        ]
256
        ordering = ["-created_at"]
1✔
257

258
    def __str__(self):
1✔
NEW
259
        return f"Report on {self.message.id} by {self.reporter.username}"
×
260

261

262
class DirectChat(models.Model):
1✔
263
    """1-on-1 chat between two users within an event"""
264

265
    event = models.ForeignKey(
1✔
266
        Event, on_delete=models.CASCADE, related_name="direct_chats"
267
    )
268
    user1 = models.ForeignKey(
1✔
269
        User, on_delete=models.CASCADE, related_name="direct_chats_initiated"
270
    )
271
    user2 = models.ForeignKey(
1✔
272
        User, on_delete=models.CASCADE, related_name="direct_chats_received"
273
    )
274
    created_at = models.DateTimeField(auto_now_add=True, db_index=True)
1✔
275
    updated_at = models.DateTimeField(auto_now=True)
1✔
276

277
    class Meta:
1✔
278
        constraints = [
1✔
279
            models.UniqueConstraint(
280
                fields=["event", "user1", "user2"], name="uniq_direct_chat"
281
            )
282
        ]
283
        indexes = [
1✔
284
            models.Index(fields=["user1", "-updated_at"]),
285
            models.Index(fields=["user2", "-updated_at"]),
286
        ]
287
        ordering = ["-updated_at"]
1✔
288

289
    def __str__(self):
1✔
290
        u1, u2 = self.user1.username, self.user2.username
1✔
291
        event_title = self.event.title
1✔
292
        return f"Chat between {u1} and {u2} in {event_title}"
1✔
293

294
    def get_other_user(self, user):
1✔
295
        """Get the other user in this chat"""
296
        return self.user2 if self.user1 == user else self.user1
1✔
297

298
    def has_user_left(self, user):
1✔
299
        """Check if a user has left this chat"""
NEW
300
        from .models import DirectChatLeave
×
301

NEW
302
        return DirectChatLeave.objects.filter(chat=self, user=user).exists()
×
303

304
    def get_active_users(self):
1✔
305
        """Get users who haven't left this chat"""
306
        from .models import DirectChatLeave
1✔
307

308
        left_users = set(
1✔
309
            DirectChatLeave.objects.filter(chat=self).values_list("user", flat=True)
310
        )
311
        return (
1✔
312
            [self.user1, self.user2]
313
            if not left_users
314
            else [u for u in [self.user1, self.user2] if u.id not in left_users]
315
        )
316

317

318
class DirectMessage(models.Model):
1✔
319
    """Messages in 1-on-1 chat"""
320

321
    chat = models.ForeignKey(
1✔
322
        DirectChat, on_delete=models.CASCADE, related_name="messages"
323
    )
324
    sender = models.ForeignKey(User, on_delete=models.CASCADE)
1✔
325
    content = models.CharField(max_length=500)
1✔
326
    is_read = models.BooleanField(default=False)
1✔
327
    created_at = models.DateTimeField(auto_now_add=True, db_index=True)
1✔
328

329
    class Meta:
1✔
330
        indexes = [
1✔
331
            models.Index(fields=["chat", "-created_at"]),
332
            models.Index(fields=["sender"]),
333
            models.Index(fields=["is_read"]),
334
        ]
335
        ordering = ["created_at"]
1✔
336

337
    def __str__(self):
1✔
338
        return f"{self.sender.username}: {self.content[:50]}"
1✔
339

340

341
class DirectChatLeave(models.Model):
1✔
342
    """Track when users leave a direct chat"""
343

344
    chat = models.ForeignKey(
1✔
345
        DirectChat, on_delete=models.CASCADE, related_name="leaves"
346
    )
347
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="chat_leaves")
1✔
348
    left_at = models.DateTimeField(auto_now_add=True, db_index=True)
1✔
349

350
    class Meta:
1✔
351
        constraints = [
1✔
352
            models.UniqueConstraint(fields=["chat", "user"], name="uniq_chat_leave")
353
        ]
354
        indexes = [
1✔
355
            models.Index(fields=["user", "-left_at"]),
356
        ]
357
        ordering = ["-left_at"]
1✔
358

359
    def __str__(self):
1✔
360
        return f"{self.user.username} left chat {self.chat.id}"
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc