|
1 | 1 | import typing |
| 2 | +import uuid |
2 | 3 |
|
3 | | -from django.core.validators import MinValueValidator |
| 4 | +from django.core.validators import MinValueValidator, MaxValueValidator |
4 | 5 | from django.db import models |
5 | 6 | from django_choices_field import IntegerChoicesField |
6 | 7 | from django_stubs_ext.db.models.manager import RelatedManager |
@@ -98,8 +99,8 @@ class Tool(UserResource): |
98 | 99 | name = models.CharField[str, str](max_length=200) |
99 | 100 | tagline = models.CharField[str, str](max_length=300, blank=True) |
100 | 101 | description = models.TextField[str, str]() |
101 | | - video_link = models.TextField[str, str](blank=True, null=True) |
102 | | - tool_link = models.TextField[str, str](blank=True, null=True) |
| 102 | + video_link = models.CharField[str, str](blank=True, null=True) |
| 103 | + tool_link = models.CharField[str, str](blank=True, null=True) |
103 | 104 | logo = models.ImageField( |
104 | 105 | upload_to="logs/", |
105 | 106 | verbose_name="Logo", |
@@ -167,3 +168,105 @@ def __str__(self): |
167 | 168 | [opt.text for opt in self.selected_options.all()], |
168 | 169 | ) |
169 | 170 | return f"{self.tool.name} - {self.question.title}: [{options}]" |
| 171 | + |
| 172 | + |
| 173 | +class UserSubmission(models.Model): |
| 174 | + """Model representing a user's submission of answers for a catalog.""" |
| 175 | + |
| 176 | + id = models.UUIDField[uuid.UUID, uuid.UUID]( |
| 177 | + primary_key=True, |
| 178 | + default=uuid.uuid4, |
| 179 | + editable=False, |
| 180 | + ) |
| 181 | + created_at = models.DateTimeField(auto_now_add=True) |
| 182 | + catalog = models.ForeignKey( |
| 183 | + Catalog, |
| 184 | + on_delete=models.CASCADE, |
| 185 | + related_name="submissions", |
| 186 | + ) |
| 187 | + |
| 188 | + # type hints |
| 189 | + answers: typing.ClassVar[RelatedManager["UserAnswer"]] |
| 190 | + results: typing.ClassVar[RelatedManager["RecommendationResult"]] |
| 191 | + |
| 192 | + class Meta(UserResource.Meta): |
| 193 | + ordering = ["-created_at"] |
| 194 | + verbose_name = "User Submission" |
| 195 | + verbose_name_plural = "User Submissions" |
| 196 | + |
| 197 | + @typing.override |
| 198 | + def __str__(self): |
| 199 | + return f"Submission {self.id} for {self.catalog.name} at {self.created_at}" |
| 200 | + |
| 201 | + |
| 202 | +class UserAnswer(models.Model): |
| 203 | + """Model representing a user's answer to a specific question.""" |
| 204 | + |
| 205 | + submission = models.ForeignKey( |
| 206 | + UserSubmission, |
| 207 | + on_delete=models.CASCADE, |
| 208 | + related_name="answers", |
| 209 | + ) |
| 210 | + question = models.ForeignKey( |
| 211 | + Question, |
| 212 | + on_delete=models.CASCADE, |
| 213 | + related_name="user_answers", |
| 214 | + ) |
| 215 | + |
| 216 | + # For ordinal questions |
| 217 | + ordinal_value: int = IntegerChoicesField( # type: ignore[reportAssignmentType] |
| 218 | + choices_enum=OrdinalTypeEnum, |
| 219 | + blank=True, |
| 220 | + null=True, |
| 221 | + ) |
| 222 | + |
| 223 | + # For checkbox questions |
| 224 | + selected_options = models.ManyToManyField( |
| 225 | + CheckboxOption, |
| 226 | + related_name="user_answers", |
| 227 | + blank=True, |
| 228 | + ) |
| 229 | + |
| 230 | + class Meta(UserResource.Meta): |
| 231 | + unique_together = ["submission", "question"] |
| 232 | + ordering = ["question__order"] |
| 233 | + verbose_name = "User Answer" |
| 234 | + verbose_name_plural = "User Answers" |
| 235 | + |
| 236 | + @typing.override |
| 237 | + def __str__(self): |
| 238 | + if self.question.question_type == QuestionTypeEnum.ORDINAL: |
| 239 | + return f"Answer to '{self.question.title}': {self.ordinal_value}" |
| 240 | + options = ", ".join([opt.text for opt in self.selected_options.all()]) |
| 241 | + return f"Answer to '{self.question.title}': [{options}]" |
| 242 | + |
| 243 | + |
| 244 | +class RecommendationResult(models.Model): |
| 245 | + """Model representing recommended tools for a submission.""" |
| 246 | + |
| 247 | + submission = models.ForeignKey( |
| 248 | + UserSubmission, |
| 249 | + on_delete=models.CASCADE, |
| 250 | + related_name="results", |
| 251 | + ) |
| 252 | + tool = models.ForeignKey( |
| 253 | + Tool, |
| 254 | + on_delete=models.CASCADE, |
| 255 | + related_name="recommendations", |
| 256 | + ) |
| 257 | + rank = models.IntegerField[int, int]( |
| 258 | + validators=[MinValueValidator(1), MaxValueValidator(5)] |
| 259 | + ) |
| 260 | + score = models.FloatField[float, float]( |
| 261 | + help_text="Proximity score - lower is better (closer match)" |
| 262 | + ) |
| 263 | + |
| 264 | + class Meta(UserResource.Meta): |
| 265 | + ordering = ["submission", "rank"] |
| 266 | + unique_together = ["submission", "rank"] |
| 267 | + verbose_name = "Recommendation Result" |
| 268 | + verbose_name_plural = "Recommendation Results" |
| 269 | + |
| 270 | + @typing.override |
| 271 | + def __str__(self): |
| 272 | + return f"#{self.rank} {self.tool.name} (score: {self.score:.2f})" |
0 commit comments