Skip to content

Commit ee4a93d

Browse files
authored
Merge pull request #201 from PROCOLLAB-github/feature/trajectories
Переработана система доступов к Навыкам
2 parents 8e02627 + a858e8c commit ee4a93d

16 files changed

Lines changed: 605 additions & 211 deletions
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Generated by Django 5.0.3 on 2025-04-03 08:32
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("courses", "0017_skill_is_from_trajectory_skill_requires_subscription"),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name="skill",
15+
name="free_access",
16+
field=models.BooleanField(
17+
default=True, help_text="Возможность проходить без подписки.", verbose_name="Доступ бесплатно"
18+
),
19+
),
20+
migrations.AlterField(
21+
model_name="task",
22+
name="free_access",
23+
field=models.BooleanField(
24+
default=True, help_text="Возможность проходить без подписки.", verbose_name="Доступ бесплатно"
25+
),
26+
),
27+
]

apps/courses/models.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class AbstractStatusField(models.Model):
2323
verbose_name="Статус",
2424
)
2525
free_access = models.BooleanField(
26-
default=False,
26+
default=True,
2727
null=False,
2828
blank=False,
2929
verbose_name="Доступ бесплатно",
@@ -93,8 +93,13 @@ def __str__(self):
9393
return f"{self.name}"
9494

9595
def clean(self):
96-
from django.core.exceptions import ValidationError
97-
96+
if not any(
97+
[self.is_from_trajectory, self.requires_subscription, self.free_access]
98+
):
99+
raise ValidationError(
100+
"Навык должен иметь хотя бы одно из значений: "
101+
"'Из траектории', 'Требует подписку' или 'Бесплатный доступ'."
102+
)
98103
if self.free_access and (self.requires_subscription or self.is_from_trajectory):
99104
raise ValidationError(
100105
"Навык не может быть одновременно бесплатным и требовать подписку или принадлежать траектории."

apps/courses/serializers.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class Meta:
4545

4646
class SkillsBasicSerializer(serializers.ModelSerializer):
4747
# Access the link field from the related FileModel
48-
file_link = serializers.URLField(source="file.link")
48+
file_link = serializers.SerializerMethodField()
4949
# Просьба захардкодить, статичный 1 уровень для всех навыков
5050
quantity_of_levels = serializers.SerializerMethodField()
5151

@@ -65,6 +65,11 @@ def get_quantity_of_levels(self, obj) -> int:
6565
# Просьба захардкодить, статичный 1 уровень для всех навыков
6666
return 1
6767

68+
def get_file_link(self, obj):
69+
if obj.file and obj.file.link:
70+
return obj.file.link
71+
return None
72+
6873

6974
class SkillsDoneSerializer(SkillsBasicSerializer):
7075
is_done = serializers.BooleanField()

apps/courses/services.py

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,25 @@
77
get_user_available_week)
88

99

10-
def get_stats(skill: Skill, profile_id: int, request_user: CustomUser | None = None) -> GetStatsDict:
10+
def get_stats(
11+
skill: Skill, profile_id: int, request_user: CustomUser | None = None
12+
) -> GetStatsDict:
1113
available_week, user = get_user_available_week(profile_id)
12-
skill_status_filter = DBObjectStatusFilters().get_skill_status_for_user(request_user)
14+
skill_status_filter = DBObjectStatusFilters().get_skill_status_for_user(
15+
request_user
16+
)
1317
available_week: int = available_week if not skill.free_access else 4
1418

1519
tasks_of_skill: QuerySet[Task] = (
16-
Task.available
17-
.only_awailable_weeks(available_week, user)
20+
Task.available.only_awailable_weeks(available_week, user)
1821
.prefetch_related(
1922
Prefetch(
2023
"task_objects__user_results",
2124
queryset=TaskObjUserResult.objects.filter(user_profile_id=profile_id),
2225
to_attr="filtered_user_results",
2326
)
24-
).prefetch_related("task_objects")
27+
)
28+
.prefetch_related("task_objects")
2529
.annotate(task_objects_count=Count("task_objects"))
2630
.filter(skill_id=skill.id)
2731
.filter(skill_status_filter)
@@ -31,7 +35,9 @@ def get_stats(skill: Skill, profile_id: int, request_user: CustomUser | None = N
3135
user_done_task_objects: list[int] = []
3236
all_task_objects: list[int] = []
3337
for task in tasks_of_skill:
34-
user_results_count = sum(1 for obj in task.task_objects.all() if obj.filtered_user_results)
38+
user_results_count = sum(
39+
1 for obj in task.task_objects.all() if obj.filtered_user_results
40+
)
3541
user_done_task_objects.append(user_results_count)
3642
all_task_objects.append(task.task_objects_count)
3743
data.append(
@@ -44,7 +50,9 @@ def get_stats(skill: Skill, profile_id: int, request_user: CustomUser | None = N
4450
"free_access": task.free_access,
4551
}
4652
)
47-
statuses = get_rounded_percentage(sum(user_done_task_objects), sum(all_task_objects))
53+
statuses = get_rounded_percentage(
54+
sum(user_done_task_objects), sum(all_task_objects)
55+
)
4856
if data:
4957
try:
5058
data.sort(key=lambda x: x["level"])
@@ -54,7 +62,9 @@ def get_stats(skill: Skill, profile_id: int, request_user: CustomUser | None = N
5462
if skill.free_access:
5563
stats_of_weeks = []
5664
else:
57-
stats_of_weeks: list[WeekStatsDict] = get_stats_of_weeks(skill, profile_id, available_week, request_user)
65+
stats_of_weeks: list[WeekStatsDict] = get_stats_of_weeks(
66+
skill, profile_id, available_week, request_user
67+
)
5868

5969
new_data = {"progress": statuses, "tasks": data, "stats_of_weeks": stats_of_weeks}
6070
return new_data
@@ -78,25 +88,28 @@ def get_stats_of_weeks(
7888
"is_done": week.is_done,
7989
"done_on_time": bool(week.additional_points) if week.is_done else None,
8090
}
81-
for week in UserWeekStat.objects.filter(user_profile__id=profile_id, skill__id=skill.id)
91+
for week in UserWeekStat.objects.filter(
92+
user_profile__id=profile_id, skill__id=skill.id
93+
)
8294
]
8395

8496
# TODO Временная мера(2 строчки ниже), как будет нормальное деление по неделям, необходимо убрать.
8597
# Выбор из минимально доступной недели и максимальной имеющейся у навыка (сейчас на проде все задания - 1неделя)
8698
# Если не убрать ничего не сломается, но лишний запрос.
8799
max_skill_week: dict[str:int] = (
88-
Task.published
89-
.for_user(request_user)
100+
Task.published.for_user(request_user)
90101
.filter(skill__id=skill.id)
91102
.aggregate(Max("week"))
92103
)
93-
available_week = min(available_week, max_skill_week["week__max"])
104+
105+
if max_skill_week["week__max"] is not None:
106+
available_week = min(available_week, max_skill_week["week__max"])
94107

95108
# Запись в `UserWeekStat` формируется при прохождении, но если юзер вообще не проходил,
96109
# необходимо дополнить список недостающими неделями (доступными ему сейчас).
97110
if len(stats_of_weeks) != available_week:
98111
existing_weeks: set[int] = {week["week"] for week in stats_of_weeks}
99-
for week in range(1, available_week+1):
112+
for week in range(1, available_week + 1):
100113
if week not in existing_weeks:
101114
stats_of_weeks.append(
102115
{

apps/courses/urls.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from django.urls import path
22

3-
from courses.views import (DoneSkillsList, SkillDetails, SkillsList, TaskList,
4-
TasksOfSkill, TaskStatsGet)
3+
from courses.views import (DoneSkillsList, SkillDetails, SkillsList,
4+
TaskDetail, TasksOfSkill, TaskStatsGet)
55

66
urlpatterns = [
7-
path("<int:task_id>", TaskList.as_view(), name="task_list"),
7+
path("<int:task_id>", TaskDetail.as_view(), name="task_detail"),
88
path("all-skills/", SkillsList.as_view(), name="all-skills"),
99
path("choose-skills/", DoneSkillsList.as_view(), name="choose-skills"),
1010
path("skill-details/<int:skill_id>", SkillDetails.as_view(), name="skill-details"),

0 commit comments

Comments
 (0)