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

gcivil-nyu-org / team4-wed-fall25 / 129

14 Dec 2025 02:09AM UTC coverage: 91.53% (+0.2%) from 91.344%
129

Pull #119

travis-pro

web-flow
Merge 31ebc2252 into 81fd4c3d1
Pull Request #119: Sync develop into main

110 of 135 new or added lines in 5 files covered. (81.48%)

2226 of 2432 relevant lines covered (91.53%)

0.92 hits per line

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

93.81
/note2webapp/models.py
1
from django.db import models
1✔
2
from django.contrib.auth.models import User
1✔
3
from django.db.models.signals import post_save
1✔
4
from django.dispatch import receiver
1✔
5
from django.db.models import Max
1✔
6

7

8
# -----------------------
9
# USER PROFILE WITH ROLE
10
# -----------------------
11
class Profile(models.Model):
1✔
12
    ROLE_CHOICES = [
1✔
13
        ("admin", "Admin"),
14
        ("uploader", "Model Uploader"),
15
        ("reviewer", "Model Reviewer"),
16
    ]
17

18
    user = models.OneToOneField(User, on_delete=models.CASCADE)
1✔
19
    # default stays "uploader" for normal signups
20
    role = models.CharField(max_length=20, choices=ROLE_CHOICES, default="uploader")
1✔
21

22
    def __str__(self):
1✔
23
        return f"{self.user.username} ({self.role})"
1✔
24

25

26
@receiver(post_save, sender=User)
1✔
27
def create_or_update_profile(sender, instance, created, **kwargs):
1✔
28
    """
29
    Ensure every User has a Profile.
30
    If the user is staff/superuser, force role="admin".
31
    """
32
    if created:
1✔
33
        profile = Profile.objects.create(user=instance)
1✔
34
    else:
35
        profile, _ = Profile.objects.get_or_create(user=instance)
1✔
36

37
    # force admin role for Django admins
38
    if instance.is_superuser or instance.is_staff:
1✔
39
        if profile.role != "admin":
1✔
40
            profile.role = "admin"
1✔
41
            profile.save()
1✔
42
    else:
43
        # just save whatever non-admin role it has
44
        profile.save()
1✔
45

46

47
# -----------------------
48
# MODEL UPLOAD
49
# -----------------------
50
class ModelUpload(models.Model):
1✔
51
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="uploads")
1✔
52
    name = models.CharField(max_length=200)
1✔
53
    created_at = models.DateTimeField(auto_now_add=True)
1✔
54

55
    def __str__(self):
1✔
56
        return self.name
1✔
57

58

59
# -----------------------
60
# MODEL VERSION
61
# -----------------------
62
class ModelVersion(models.Model):
1✔
63
    upload = models.ForeignKey(
1✔
64
        ModelUpload,
65
        on_delete=models.CASCADE,
66
        related_name="versions",
67
    )
68

69
    # uploaded files
70
    model_file = models.FileField(upload_to="models/")
1✔
71
    predict_file = models.FileField(upload_to="predict/")
1✔
72
    schema_file = models.FileField(upload_to="schemas/", blank=True, null=True)
1✔
73

74
    tag = models.CharField(max_length=100)
1✔
75
    information = models.TextField(blank=True, null=True)
1✔
76

77
    CATEGORY_CHOICES = [
1✔
78
        ("sentiment", "Sentiment"),
79
        ("recommendation", "Recommendation"),
80
        ("text-classification", "Text Classification"),
81
    ]
82
    category = models.CharField(
1✔
83
        max_length=50,
84
        choices=CATEGORY_CHOICES,
85
        default="sentiment",
86
    )
87

88
    status = models.CharField(
1✔
89
        max_length=20,
90
        choices=[
91
            ("PENDING", "Pending"),
92
            ("PASS", "Pass"),
93
            ("FAIL", "Fail"),
94
        ],
95
        default="PENDING",
96
    )
97

98
    is_active = models.BooleanField(default=False)
1✔
99
    log = models.TextField(blank=True, null=True)
1✔
100
    created_at = models.DateTimeField(auto_now_add=True)
1✔
101
    usage_count = models.PositiveIntegerField(
1✔
102
        default=0, help_text="Number of times this version has been tested."
103
    )
104

105
    # soft delete
106
    is_deleted = models.BooleanField(default=False)
1✔
107
    deleted_at = models.DateTimeField(null=True, blank=True)
1✔
108

109
    # per-upload version number
110
    version_number = models.IntegerField(default=1)
1✔
111

112
    # hashes for dedupe
113
    model_hash = models.CharField(max_length=64, blank=True, null=True)
1✔
114
    predict_hash = models.CharField(max_length=64, blank=True, null=True)
1✔
115
    schema_hash = models.CharField(max_length=64, blank=True, null=True)
1✔
116
    bundle_hash = models.CharField(max_length=64, blank=True, null=True)
1✔
117

118
    class Meta:
1✔
119
        ordering = ["-created_at"]
1✔
120

121
    def save(self, *args, **kwargs):
1✔
122
        # assign next version number only on first save
123
        if not self.pk:
1✔
124
            last_version = ModelVersion.objects.filter(upload=self.upload).aggregate(
1✔
125
                Max("version_number")
126
            )["version_number__max"]
127
            self.version_number = (last_version or 0) + 1
1✔
128
        super().save(*args, **kwargs)
1✔
129

130
    def __str__(self):
1✔
131
        try:
1✔
132
            return f"{self.upload.name} - v{self.version_number} ({self.status})"
1✔
133
        except Exception:
1✔
134
            return f"ModelVersion (unsaved) - {self.status}"
1✔
135

136
    def get_version_number(self):
1✔
137
        return self.version_number
1✔
138

139
    def get_media_dir(self):
1✔
140
        """
141
        media/<category>/<upload.name>/v<version_number>/
142
        """
143
        safe_name = getattr(self.upload, "name", f"upload-{self.upload_id}")
1✔
144
        return f"{self.category}/{safe_name}/v{self.version_number}/"
1✔
145

146

147
# -----------------------
148
# MODEL FEEDBACK/COMMENT SYSTEM
149
# -----------------------
150
class ModelComment(models.Model):
1✔
151
    model_version = models.ForeignKey(
1✔
152
        ModelVersion, on_delete=models.CASCADE, related_name="comments"
153
    )
154
    user = models.ForeignKey(User, on_delete=models.CASCADE)
1✔
155
    content = models.TextField()
1✔
156
    parent = models.ForeignKey(
1✔
157
        "self", null=True, blank=True, on_delete=models.CASCADE, related_name="replies"
158
    )
159
    created_at = models.DateTimeField(auto_now_add=True)
1✔
160
    updated_at = models.DateTimeField(auto_now=True)
1✔
161

162
    class Meta:
1✔
163
        ordering = ["created_at"]
1✔
164

165
    def __str__(self):
1✔
166
        return f"{self.user.username} on {self.model_version.tag}: {self.content[:50]}"
1✔
167

168
    def is_reply(self):
1✔
169
        return self.parent is not None
1✔
170

171
    def get_likes_count(self):
1✔
172
        return self.reactions.filter(reaction_type="like").count()
1✔
173

174
    def get_dislikes_count(self):
1✔
175
        return self.reactions.filter(reaction_type="dislike").count()
1✔
176

177
    def get_user_reaction(self, user):
1✔
178
        try:
×
179
            reaction = self.reactions.get(user=user)
×
180
            return reaction.reaction_type
×
181
        except CommentReaction.DoesNotExist:
×
182
            return None
×
183

184

185
class CommentReaction(models.Model):
1✔
186
    REACTION_CHOICES = [
1✔
187
        ("like", "Like"),
188
        ("dislike", "Dislike"),
189
    ]
190

191
    comment = models.ForeignKey(
1✔
192
        ModelComment, on_delete=models.CASCADE, related_name="reactions"
193
    )
194
    user = models.ForeignKey(User, on_delete=models.CASCADE)
1✔
195
    reaction_type = models.CharField(max_length=10, choices=REACTION_CHOICES)
1✔
196
    created_at = models.DateTimeField(auto_now_add=True)
1✔
197

198
    class Meta:
1✔
199
        unique_together = ["comment", "user"]
1✔
200
        ordering = ["-created_at"]
1✔
201

202
    def __str__(self):
1✔
203
        return f"{self.user.username} {self.reaction_type}s comment {self.comment.id}"
×
204

205

206
class Notification(models.Model):
1✔
207
    TARGET_TYPES = [
1✔
208
        ("comment", "Comment"),
209
        ("version", "Model Version"),
210
        ("model", "Model Upload"),
211
    ]
212

213
    user = models.ForeignKey(
1✔
214
        User,
215
        on_delete=models.CASCADE,
216
        related_name="notifications",
217
    )
218
    actor = models.ForeignKey(
1✔
219
        User,
220
        on_delete=models.SET_NULL,
221
        null=True,
222
        blank=True,
223
        related_name="actor_notifications",
224
    )
225

226
    # 👇 THIS is what list_notifications is trying to read
227
    verb = models.CharField(max_length=255, blank=True)
1✔
228

229
    target_type = models.CharField(
1✔
230
        max_length=50,
231
        choices=TARGET_TYPES,
232
        null=True,
233
        blank=True,
234
    )
235
    target_id = models.PositiveIntegerField(null=True, blank=True)
1✔
236

237
    extra = models.JSONField(default=dict, blank=True)
1✔
238

239
    is_read = models.BooleanField(default=False)
1✔
240
    created_at = models.DateTimeField(auto_now_add=True)
1✔
241

242
    class Meta:
1✔
243
        ordering = ["-created_at"]
1✔
244

245
    def __str__(self):
1✔
NEW
246
        return f"Notification for {self.user} – {self.verb or 'Notification'}"
×
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