Skip to content

Commit 759f452

Browse files
authored
Merge pull request #200 from PROCOLLAB-github/dev
Обновлён модуль "Траектории"
2 parents bb77634 + 8e02627 commit 759f452

10 files changed

Lines changed: 467 additions & 57 deletions
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Generated by Django 5.0.3 on 2025-03-26 09:02
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("courses", "0016_skill_free_access_task_free_access"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="skill",
15+
name="is_from_trajectory",
16+
field=models.BooleanField(
17+
default=False,
18+
help_text="Указывает, что навык доступен только в рамках траектории.",
19+
verbose_name="Из траектории",
20+
),
21+
),
22+
migrations.AddField(
23+
model_name="skill",
24+
name="requires_subscription",
25+
field=models.BooleanField(
26+
default=False,
27+
help_text="Определяет, необходима ли подписка для доступа к навыку.",
28+
verbose_name="Требует подписку",
29+
),
30+
),
31+
]

apps/courses/models.py

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class AbstractStatusField(models.Model):
2727
null=False,
2828
blank=False,
2929
verbose_name="Доступ бесплатно",
30-
help_text="Возможность проходить без подписки."
30+
help_text="Возможность проходить без подписки.",
3131
)
3232

3333
objects = models.Manager()
@@ -38,7 +38,16 @@ class Meta:
3838

3939

4040
class Skill(AbstractStatusField):
41-
41+
is_from_trajectory = models.BooleanField(
42+
default=False,
43+
verbose_name="Из траектории",
44+
help_text="Указывает, что навык доступен только в рамках траектории.",
45+
)
46+
requires_subscription = models.BooleanField(
47+
default=False,
48+
verbose_name="Требует подписку",
49+
help_text="Определяет, необходима ли подписка для доступа к навыку.",
50+
)
4251
name = models.CharField(max_length=50, verbose_name="Название навыка")
4352
description = models.TextField(null=True)
4453
who_created = models.CharField(max_length=50, verbose_name="Кто создал")
@@ -76,18 +85,32 @@ class Skill(AbstractStatusField):
7685
verbose_name="Тип подписки",
7786
)
7887

79-
def __str__(self):
80-
return f"{self.name}"
81-
8288
class Meta:
8389
verbose_name = "Навык"
8490
verbose_name_plural = "Навыки"
8591

92+
def __str__(self):
93+
return f"{self.name}"
94+
95+
def clean(self):
96+
from django.core.exceptions import ValidationError
97+
98+
if self.free_access and (self.requires_subscription or self.is_from_trajectory):
99+
raise ValidationError(
100+
"Навык не может быть одновременно бесплатным и требовать подписку или принадлежать траектории."
101+
)
102+
103+
super().clean()
104+
86105
def save(self, *args, **kwargs):
87106
if self.pk is None: # Проверка, сохранен ли объект в базе данных
88-
super().save(*args, **kwargs) # Если объект еще не сохранен, сохраняем его сначала
107+
super().save(
108+
*args, **kwargs
109+
) # Если объект еще не сохранен, сохраняем его сначала
89110

90-
quantity_unique_levels = Task.objects.filter(skill=self).values("level").distinct().count()
111+
quantity_unique_levels = (
112+
Task.objects.filter(skill=self).values("level").distinct().count()
113+
)
91114
self.quantity_of_levels = quantity_unique_levels
92115

93116
super().save(*args, **kwargs)
@@ -107,9 +130,13 @@ class Task(AbstractStatusField):
107130
help_text="Если не указать, то автоматически станет последним в порядке показа",
108131
)
109132
name = models.CharField(max_length=50, verbose_name="Название")
110-
skill = models.ForeignKey(Skill, on_delete=models.CASCADE, related_name="tasks", verbose_name="Навык")
133+
skill = models.ForeignKey(
134+
Skill, on_delete=models.CASCADE, related_name="tasks", verbose_name="Навык"
135+
)
111136
level = models.IntegerField(default=1, verbose_name="Уровень")
112-
week = models.PositiveSmallIntegerField(choices=WEEK_CHOICES, default=1, verbose_name="Неделя")
137+
week = models.PositiveSmallIntegerField(
138+
choices=WEEK_CHOICES, default=1, verbose_name="Неделя"
139+
)
113140

114141
objects = models.Manager()
115142
available = AvailableForUser()
@@ -145,10 +172,15 @@ class TaskObject(models.Model):
145172
verbose_name="Задача",
146173
)
147174
content_type = models.ForeignKey(
148-
ContentType, on_delete=models.CASCADE, related_name="task_objects_content", verbose_name="Тип единицы задачи"
175+
ContentType,
176+
on_delete=models.CASCADE,
177+
related_name="task_objects_content",
178+
verbose_name="Тип единицы задачи",
149179
)
150180
object_id = models.PositiveIntegerField(verbose_name="ID единицы задачи")
151-
popup = models.ManyToManyField("Popup", blank=True, related_name="task_objects", verbose_name="Поп-ап")
181+
popup = models.ManyToManyField(
182+
"Popup", blank=True, related_name="task_objects", verbose_name="Поп-ап"
183+
)
152184
content_object = GenericForeignKey("content_type", "object_id")
153185
validate_answer = models.BooleanField(
154186
default=True,
@@ -180,12 +212,21 @@ def save(self, *args, **kwargs):
180212

181213

182214
class Popup(models.Model):
183-
title = models.CharField(null=True, blank=True, max_length=150, verbose_name="Заголовок")
215+
title = models.CharField(
216+
null=True, blank=True, max_length=150, verbose_name="Заголовок"
217+
)
184218
text = models.TextField(null=True, blank=True, verbose_name="Содержимое")
185219
file = models.ForeignKey(
186-
FileModel, null=True, blank=True, on_delete=models.PROTECT, related_name="popups", verbose_name="Изображение"
220+
FileModel,
221+
null=True,
222+
blank=True,
223+
on_delete=models.PROTECT,
224+
related_name="popups",
225+
verbose_name="Изображение",
226+
)
227+
ordinal_number = models.PositiveSmallIntegerField(
228+
null=True, blank=True, verbose_name="Порядковый номер"
187229
)
188-
ordinal_number = models.PositiveSmallIntegerField(null=True, blank=True, verbose_name="Порядковый номер")
189230

190231
class Meta:
191232
verbose_name = "Поп-ап"
@@ -197,7 +238,8 @@ def __str__(self):
197238
def clean(self):
198239
if not self.title and not self.text and not self.file:
199240
raise ValidationError(
200-
"Должено быть заполнено хотя бы один из полей: " "'Заголовок', 'Содержимое' или 'Изображение'"
241+
"Должено быть заполнено хотя бы один из полей: "
242+
"'Заголовок', 'Содержимое' или 'Изображение'"
201243
)
202244

203245
def save(self, *args, **kwargs):

apps/courses/serializers.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ class TaskSerializer(serializers.Serializer):
1919
skill_name = serializers.CharField(allow_null=True)
2020
skill_preview = serializers.CharField(allow_null=True)
2121
skill_point_logo = serializers.CharField(allow_null=True)
22-
count = serializers.IntegerField(help_text="количество вопросов и информационных слайдов у задания")
22+
count = serializers.IntegerField(
23+
help_text="количество вопросов и информационных слайдов у задания"
24+
)
2325
free_access = serializers.BooleanField(allow_null=True)
2426
step_data = StepSerializer(many=True)
2527

@@ -82,7 +84,7 @@ class Meta:
8284

8385

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

8789
class Meta:
8890
model = Skill
@@ -99,7 +101,9 @@ class Meta:
99101
dataclass = PopupSerializerData
100102

101103

102-
IntegerListSerializer = serializers.ListSerializer(child=serializers.IntegerField(), allow_empty=False)
104+
IntegerListSerializer = serializers.ListSerializer(
105+
child=serializers.IntegerField(), allow_empty=False
106+
)
103107

104108

105109
class SkillDetailsSerializer(serializers.ModelSerializer):

apps/trajectories/admin.py

Lines changed: 32 additions & 7 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,9 +56,15 @@ class UserTrajectoryAdmin(admin.ModelAdmin):
4956
"is_active",
5057
"mentor",
5158
)
52-
list_filter = ("is_active", "trajectory",)
53-
search_fields = ("user__email", "trajectory__name",)
54-
autocomplete_fields = ("mentor",)
59+
list_filter = (
60+
"is_active",
61+
"trajectory",
62+
)
63+
search_fields = (
64+
"user__email",
65+
"trajectory__name",
66+
)
67+
autocomplete_fields = ("user", "mentor",)
5568

5669
def trajectory_name(self, obj):
5770
return obj.trajectory.name
@@ -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+
]

0 commit comments

Comments
 (0)