Skip to content

Commit 63e7638

Browse files
committed
Add models for recommendations
1 parent 2b91600 commit 63e7638

File tree

4 files changed

+206
-13
lines changed

4 files changed

+206
-13
lines changed

apps/tool_picker/admin.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,11 @@ class QuestionInline(admin.StackedInline): # type: ignore[reportMissingTypeArgu
3232
show_change_link = True
3333

3434

35-
# Inline for Tools within Catalog
36-
class ToolInline(admin.TabularInline): # type: ignore[reportMissingTypeArgument]
37-
model = Tool
38-
extra = 0
39-
fields = ["name", "tagline"]
40-
show_change_link = True
41-
42-
4335
@admin.register(Catalog)
4436
class CatalogAdmin(UserResourceAdmin, admin.ModelAdmin): # type: ignore[reportMissingTypeArgument]
4537
list_display = ["name", "question_count", "tool_count"]
4638
search_fields = ["name", "description"]
47-
inlines = [QuestionInline, ToolInline]
39+
inlines = [QuestionInline]
4840

4941
@admin.display(description="Questions")
5042
def question_count(self, obj: Catalog):
@@ -251,7 +243,12 @@ class ToolAdmin(UserResourceAdmin, admin.ModelAdmin): # type: ignore[reportMiss
251243
(
252244
"Basic Information",
253245
{
254-
"fields": ("catalog", "name", "tagline", "description"),
246+
"fields": (
247+
"catalog", "name",
248+
"tagline", "description",
249+
"video_link", "tool_link",
250+
"logo",
251+
),
255252
},
256253
),
257254
)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Generated by Django 5.2.9 on 2026-01-05 10:27
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('tool_picker', '0002_initial'),
10+
]
11+
12+
operations = [
13+
migrations.AlterModelOptions(
14+
name='checkboxquestionproxy',
15+
options={'ordering': ['-id'], 'verbose_name': 'Checkbox Question (Bulk Edit)', 'verbose_name_plural': 'Checkbox Questions (Bulk Edit)'},
16+
),
17+
migrations.AlterField(
18+
model_name='tool',
19+
name='tool_link',
20+
field=models.CharField(blank=True, null=True),
21+
),
22+
migrations.AlterField(
23+
model_name='tool',
24+
name='video_link',
25+
field=models.CharField(blank=True, null=True),
26+
),
27+
]
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Generated by Django 5.2.9 on 2026-01-05 12:11
2+
3+
import apps.tool_picker.models
4+
import django.core.validators
5+
import django.db.models.deletion
6+
import django_choices_field.fields
7+
import uuid
8+
from django.db import migrations, models
9+
10+
11+
class Migration(migrations.Migration):
12+
13+
dependencies = [
14+
('tool_picker', '0003_alter_checkboxquestionproxy_options_and_more'),
15+
]
16+
17+
operations = [
18+
migrations.CreateModel(
19+
name='UserSubmission',
20+
fields=[
21+
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
22+
('created_at', models.DateTimeField(auto_now_add=True)),
23+
('catalog', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='tool_picker.catalog')),
24+
],
25+
options={
26+
'verbose_name': 'User Submission',
27+
'verbose_name_plural': 'User Submissions',
28+
'ordering': ['-created_at'],
29+
'abstract': False,
30+
},
31+
),
32+
migrations.CreateModel(
33+
name='UserAnswer',
34+
fields=[
35+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
36+
('ordinal_value', django_choices_field.fields.IntegerChoicesField(blank=True, choices=[(10, 'n/a'), (11, '1'), (12, '2'), (13, '3'), (14, '4')], choices_enum=apps.tool_picker.models.OrdinalTypeEnum, null=True)),
37+
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_answers', to='tool_picker.question')),
38+
('selected_options', models.ManyToManyField(blank=True, related_name='user_answers', to='tool_picker.checkboxoption')),
39+
('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='tool_picker.usersubmission')),
40+
],
41+
options={
42+
'verbose_name': 'User Answer',
43+
'verbose_name_plural': 'User Answers',
44+
'ordering': ['question__order'],
45+
'abstract': False,
46+
'unique_together': {('submission', 'question')},
47+
},
48+
),
49+
migrations.CreateModel(
50+
name='RecommendationResult',
51+
fields=[
52+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
53+
('rank', models.IntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)])),
54+
('score', models.FloatField(help_text='Proximity score - lower is better (closer match)')),
55+
('tool', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='recommendations', to='tool_picker.tool')),
56+
('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='results', to='tool_picker.usersubmission')),
57+
],
58+
options={
59+
'verbose_name': 'Recommendation Result',
60+
'verbose_name_plural': 'Recommendation Results',
61+
'ordering': ['submission', 'rank'],
62+
'abstract': False,
63+
'unique_together': {('submission', 'rank')},
64+
},
65+
),
66+
]

apps/tool_picker/models.py

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import typing
2+
import uuid
23

3-
from django.core.validators import MinValueValidator
4+
from django.core.validators import MinValueValidator, MaxValueValidator
45
from django.db import models
56
from django_choices_field import IntegerChoicesField
67
from django_stubs_ext.db.models.manager import RelatedManager
@@ -98,8 +99,8 @@ class Tool(UserResource):
9899
name = models.CharField[str, str](max_length=200)
99100
tagline = models.CharField[str, str](max_length=300, blank=True)
100101
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)
103104
logo = models.ImageField(
104105
upload_to="logs/",
105106
verbose_name="Logo",
@@ -167,3 +168,105 @@ def __str__(self):
167168
[opt.text for opt in self.selected_options.all()],
168169
)
169170
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

Comments
 (0)