Skip to content

Commit 9abb00d

Browse files
authored
Merge pull request #198 from PROCOLLAB-github/feature/trajectories
Добавлены индивидуальные навыки в рамках модуля Траектория
2 parents cf3b3e8 + 22c2cc7 commit 9abb00d

File tree

8 files changed

+351
-36
lines changed

8 files changed

+351
-36
lines changed

apps/courses/serializers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ class Meta:
8282

8383

8484
class SkillNameAndLogoSerializer(serializers.ModelSerializer):
85-
file_link = serializers.URLField(source="file.link")
85+
file_link = serializers.URLField(source="file.link", default=None)
8686

8787
class Meta:
8888
model = Skill

apps/trajectories/admin.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from django.contrib import admin
22

3-
from .models import Meeting, Month, Trajectory, UserTrajectory
3+
from .models import (Meeting, Month, Trajectory, UserIndividualSkill,
4+
UserTrajectory)
45

56

67
@admin.register(Trajectory)
@@ -12,8 +13,14 @@ class TrajectoryAdmin(admin.ModelAdmin):
1213
"start_date",
1314
"duration_months",
1415
)
15-
list_filter = ("company", "start_date",)
16-
search_fields = ("name", "company",)
16+
list_filter = (
17+
"company",
18+
"start_date",
19+
)
20+
search_fields = (
21+
"name",
22+
"company",
23+
)
1724
autocomplete_fields = ("mentors",)
1825

1926
def get_mentors(self, obj):
@@ -49,8 +56,14 @@ class UserTrajectoryAdmin(admin.ModelAdmin):
4956
"is_active",
5057
"mentor",
5158
)
52-
list_filter = ("is_active", "trajectory",)
53-
search_fields = ("user__email", "trajectory__name",)
59+
list_filter = (
60+
"is_active",
61+
"trajectory",
62+
)
63+
search_fields = (
64+
"user__email",
65+
"trajectory__name",
66+
)
5467
autocomplete_fields = ("mentor",)
5568

5669
def trajectory_name(self, obj):
@@ -68,10 +81,22 @@ class MeetingAdmin(admin.ModelAdmin):
6881
"final_meeting",
6982
)
7083
readonly_fields = ("user_trajectory",)
71-
list_filter = ("initial_meeting", "final_meeting",)
84+
list_filter = (
85+
"initial_meeting",
86+
"final_meeting",
87+
)
7288
search_fields = ("user_trajectory__user__email",)
7389

7490
def user_trajectory_user_email(self, obj):
7591
return obj.user_trajectory.user.email
7692

7793
user_trajectory_user_email.short_description = "Пользователь"
94+
95+
96+
@admin.register(UserIndividualSkill)
97+
class UserIndividualSkillAdmin(admin.ModelAdmin):
98+
exclude = ("user_trajectory",)
99+
autocomplete_fields = ["user"]
100+
101+
def get_readonly_fields(self, request, obj=None):
102+
return ["user_trajectory"] if obj else []
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Generated by Django 5.0.3 on 2025-03-13 09:59
2+
3+
from django.db import migrations, models
4+
5+
import trajectories.validators
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
("trajectories", "0002_alter_month_options_month_order"),
12+
]
13+
14+
operations = [
15+
migrations.AlterField(
16+
model_name="trajectory",
17+
name="background_color",
18+
field=models.CharField(
19+
default="#FFFFFF",
20+
max_length=7,
21+
validators=[trajectories.validators.validate_hex_color],
22+
verbose_name="Цвет заднего фона билета",
23+
),
24+
),
25+
migrations.AlterField(
26+
model_name="trajectory",
27+
name="button_color",
28+
field=models.CharField(
29+
default="#6c27ff",
30+
max_length=7,
31+
validators=[trajectories.validators.validate_hex_color],
32+
verbose_name="Цвет кнопки 'Подробнее'",
33+
),
34+
),
35+
migrations.AlterField(
36+
model_name="trajectory",
37+
name="duration_months",
38+
field=models.IntegerField(
39+
validators=[trajectories.validators.validate_positive], verbose_name="Количество месяцев"
40+
),
41+
),
42+
migrations.AlterField(
43+
model_name="trajectory",
44+
name="select_button_color",
45+
field=models.CharField(
46+
default="#6c27ff",
47+
max_length=7,
48+
validators=[trajectories.validators.validate_hex_color],
49+
verbose_name="Цвет кнопки 'Выбрать'",
50+
),
51+
),
52+
migrations.AlterField(
53+
model_name="trajectory",
54+
name="text_color",
55+
field=models.CharField(
56+
default="#332e2d",
57+
max_length=7,
58+
validators=[trajectories.validators.validate_hex_color],
59+
verbose_name="Цвет текста на билете",
60+
),
61+
),
62+
]
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Generated by Django 5.0.3 on 2025-03-24 11:04
2+
3+
import django.db.models.deletion
4+
import django.utils.timezone
5+
from django.conf import settings
6+
from django.db import migrations, models
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
("courses", "0016_skill_free_access_task_free_access"),
13+
("trajectories", "0003_alter_trajectory_background_color_and_more"),
14+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15+
]
16+
17+
operations = [
18+
migrations.CreateModel(
19+
name="UserIndividualSkill",
20+
fields=[
21+
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
22+
("start_date", models.DateTimeField(default=django.utils.timezone.now, verbose_name="Дата добавления")),
23+
("skills", models.ManyToManyField(to="courses.skill", verbose_name="Навыки")),
24+
(
25+
"user",
26+
models.ForeignKey(
27+
on_delete=django.db.models.deletion.CASCADE,
28+
related_name="individual_skills",
29+
to=settings.AUTH_USER_MODEL,
30+
verbose_name="Пользователь",
31+
),
32+
),
33+
(
34+
"user_trajectory",
35+
models.ForeignKey(
36+
editable=False,
37+
on_delete=django.db.models.deletion.CASCADE,
38+
related_name="individual_skills",
39+
to="trajectories.usertrajectory",
40+
verbose_name="Активная траектория",
41+
),
42+
),
43+
],
44+
options={
45+
"verbose_name": "Индивидуальный навык",
46+
"verbose_name_plural": "Индивидуальные навыки",
47+
},
48+
),
49+
]

apps/trajectories/models.py

Lines changed: 97 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,37 @@ class Trajectory(models.Model):
2828
null=True,
2929
blank=True,
3030
)
31-
mentors = models.ManyToManyField(CustomUser, related_name="mentored_trajectories", verbose_name="Наставники")
31+
mentors = models.ManyToManyField(
32+
CustomUser, related_name="mentored_trajectories", verbose_name="Наставники"
33+
)
3234
start_date = models.DateField(default=timezone.now, verbose_name="Дата начала")
33-
duration_months = models.IntegerField(verbose_name="Количество месяцев", validators=[validate_positive])
35+
duration_months = models.IntegerField(
36+
verbose_name="Количество месяцев", validators=[validate_positive]
37+
)
3438
company = models.CharField(max_length=255, verbose_name="Компания")
3539
background_color = models.CharField(
36-
max_length=7, default="#FFFFFF", verbose_name="Цвет заднего фона билета", validators=[validate_hex_color]
40+
max_length=7,
41+
default="#FFFFFF",
42+
verbose_name="Цвет заднего фона билета",
43+
validators=[validate_hex_color],
3744
)
3845
button_color = models.CharField(
39-
max_length=7, default="#6c27ff", verbose_name="Цвет кнопки 'Подробнее'", validators=[validate_hex_color]
46+
max_length=7,
47+
default="#6c27ff",
48+
verbose_name="Цвет кнопки 'Подробнее'",
49+
validators=[validate_hex_color],
4050
)
4151
select_button_color = models.CharField(
42-
max_length=7, default="#6c27ff", verbose_name="Цвет кнопки 'Выбрать'", validators=[validate_hex_color]
52+
max_length=7,
53+
default="#6c27ff",
54+
verbose_name="Цвет кнопки 'Выбрать'",
55+
validators=[validate_hex_color],
4356
)
4457
text_color = models.CharField(
45-
max_length=7, default="#332e2d", verbose_name="Цвет текста на билете", validators=[validate_hex_color]
58+
max_length=7,
59+
default="#332e2d",
60+
verbose_name="Цвет текста на билете",
61+
validators=[validate_hex_color],
4662
)
4763

4864
def __str__(self):
@@ -59,7 +75,9 @@ class Month(models.Model):
5975
Связана с траекторией и набором навыков.
6076
"""
6177

62-
trajectory = models.ForeignKey(Trajectory, on_delete=models.CASCADE, related_name="months")
78+
trajectory = models.ForeignKey(
79+
Trajectory, on_delete=models.CASCADE, related_name="months"
80+
)
6381
skills = models.ManyToManyField("courses.Skill", verbose_name="Навыки")
6482
order = models.PositiveIntegerField(verbose_name="Порядковый номер месяца")
6583

@@ -90,7 +108,9 @@ def clean(self):
90108
"""
91109
current_months_count = self.trajectory.months.count()
92110
if current_months_count >= self.trajectory.duration_months:
93-
raise ValidationError(f"Нельзя добавить больше {self.trajectory.duration_months} месяцев в эту траекторию.")
111+
raise ValidationError(
112+
f"Нельзя добавить больше {self.trajectory.duration_months} месяцев в эту траекторию."
113+
)
94114

95115
def save(self, *args, **kwargs):
96116
self.clean()
@@ -108,12 +128,18 @@ class UserTrajectory(models.Model):
108128
Каждый пользователь может быть привязан только к одной активной траектории в любой момент времени.
109129
"""
110130

111-
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE, related_name="user_trajectories")
131+
user = models.ForeignKey(
132+
CustomUser, on_delete=models.CASCADE, related_name="user_trajectories"
133+
)
112134
trajectory = models.ForeignKey(Trajectory, on_delete=models.CASCADE)
113135
start_date = models.DateField(default=timezone.now)
114136
is_active = models.BooleanField(default=True)
115137
mentor = models.ForeignKey(
116-
CustomUser, on_delete=models.SET_NULL, null=True, blank=True, related_name="mentored_users"
138+
CustomUser,
139+
on_delete=models.SET_NULL,
140+
null=True,
141+
blank=True,
142+
related_name="mentored_users",
117143
)
118144

119145
def __str__(self):
@@ -141,13 +167,72 @@ class Meta:
141167
verbose_name_plural = "Пользовательские траектории"
142168

143169

170+
class UserIndividualSkill(models.Model):
171+
"""
172+
Модель для хранения индивидуальных навыков пользователя в рамках активной траектории.
173+
Поле user_trajectory заполняется автоматически.
174+
"""
175+
176+
user = models.ForeignKey(
177+
CustomUser,
178+
on_delete=models.CASCADE,
179+
related_name="individual_skills",
180+
verbose_name="Пользователь",
181+
)
182+
user_trajectory = models.ForeignKey(
183+
UserTrajectory,
184+
on_delete=models.CASCADE,
185+
related_name="individual_skills",
186+
verbose_name="Активная траектория",
187+
editable=False,
188+
)
189+
skills = models.ManyToManyField("courses.Skill", verbose_name="Навыки")
190+
start_date = models.DateTimeField(
191+
default=timezone.now, verbose_name="Дата добавления"
192+
)
193+
194+
class Meta:
195+
verbose_name = "Индивидуальный навык"
196+
verbose_name_plural = "Индивидуальные навыки"
197+
198+
def __str__(self):
199+
return f"{self.user.email} (Траектория: {self.user_trajectory.trajectory.name})"
200+
201+
def clean(self):
202+
active_trajectory = self.user.user_trajectories.filter(is_active=True).first()
203+
204+
if not active_trajectory:
205+
raise ValidationError(
206+
"Невозможно сохранить навык: у пользователя нет активной траектории."
207+
)
208+
209+
if not self.user_trajectory_id:
210+
self.user_trajectory = active_trajectory
211+
212+
if self.user_trajectory.user != self.user:
213+
raise ValidationError("Траектория не принадлежит указанному пользователю.")
214+
215+
super().clean()
216+
217+
def save(self, *args, **kwargs):
218+
"""
219+
Удаляем явный вызов clean(), так как он вызывается автоматически
220+
через full_clean() при сохранении через формы/админку
221+
"""
222+
super().save(*args, **kwargs)
223+
224+
144225
class Meeting(models.Model):
145226
"""
146227
Модель для отражения статуса встреч пользователя с наставником в рамках траектории.
147228
"""
148229

149-
user_trajectory = models.ForeignKey(UserTrajectory, on_delete=models.CASCADE, related_name="meetings")
150-
initial_meeting = models.BooleanField(default=False, verbose_name="Начальная встреча")
230+
user_trajectory = models.ForeignKey(
231+
UserTrajectory, on_delete=models.CASCADE, related_name="meetings"
232+
)
233+
initial_meeting = models.BooleanField(
234+
default=False, verbose_name="Начальная встреча"
235+
)
151236
final_meeting = models.BooleanField(default=False, verbose_name="Финальная встреча")
152237

153238
def __str__(self):

0 commit comments

Comments
 (0)