У меня есть менеджер, который считает все освоенные навыки, за счёт подсчёта сессий по учебным материалам, прикреплённым к данному навыку.
class Trajectory(CommonSystemObjectModel):
actives = models.ManyToManyField(
'actives.Active',
verbose_name='Цели профиля',
through='actives.TrajectoryActive',
blank=True
)
is_active = models.BooleanField(
default=True,
verbose_name='Активен ли профиль в данный момент'
)
class TrajectoryActive(CommonPriorityDataAbstractModel):
trajectory = models.ForeignKey(
to='trajectories.Trajectory',
on_delete=models.CASCADE,
verbose_name='Профиль',
related_name='trajectory_actives',
db_index=True
)
active: Active = models.ForeignKey(
to=Active,
on_delete=models.CASCADE,
verbose_name='Цель',
related_name='active_trajectories',
db_index=True
)
threats = models.ManyToManyField(
to='threats.Threat',
verbose_name='Навыки',
through='threats.ActiveThreat'
)
Модель навыка является расширение для m2m поля threats для модели TrajectoryActive, которая в свою очередь является расширением m2m поля actives для модели Trajectory (возможно из-за этого django сбоит)
class ActiveThreat(CommonPriorityDataAbstractModel):
"""Модель навыка"""
threat: Threat = models.ForeignKey(
to=Threat,
on_delete=models.CASCADE,
verbose_name='Навык',
db_index=True,
related_name='threat_actives'
)
trajectory_active = models.ForeignKey(
to='actives.TrajectoryActive',
on_delete=models.CASCADE,
db_index=True,
verbose_name='Цель, прикрепленная к профилю',
related_name='active_threats'
)
content = models.ManyToManyField(
CommonContentDataModel,
verbose_name='УМы, которые прикреплены к навыку',
blank=True,
db_index=True,
related_name='content_threats'
)
objects = ActiveThreatManager()
Менеджер
class ActiveThreatManager(CommonObjectContentTypeManager):
@staticmethod
def _get_is_completed_case() -> Case:
"""Навык является освоенным,
если кол-во УМов больше 0 и кол-во юзеров больше 0 и
кол-во сессий по УМам навыка >= кол-ву юзеров * кол-во УМов"""
return Case(
When(
Q(users_count__gt=0) &
Q(content_count__gt=0) &
Q(content__isnull=False) &
Q(completed_sessions_count__gte=F('users_count') * F('content_count')),
then=True,
),
default=False,
output_field=models.BooleanField()
)
def default_metrics(self, is_completed: bool = None, **kwargs) -> QuerySet:
"""Получаем метрику навыков цели для подсчёта метрики целей профиля"""
qs = super().get_queryset().filter(**kwargs).annotate(
content_count=Count('content', distinct=True), # выдаёт неверные результаты, независимо от distinct( без disitinct больше)
completed_sessions_count=Count( # также выдаёт неверные результаты, т.к. ссылается на m2m поле content
'content__sessions',
filter=Q(
content__sessions__completion_status=COMPLETED_KEY,
content__sessions__user=F('trajectory_active__trajectory__users'),
content__sessions__completion_datetime__isnull=False
),
distinct=True
),
users_count=Count('trajectory_active__trajectory__users', distinct=True)
).annotate(
is_completed=self._get_is_completed_case()
)
return qs.filter(is_completed=is_completed) if is_completed else qs
# У каждого навыка аннотируется поле content_count
skill.content.add(conten1, content2)
skill.content.count() # выдаёт 2
skill = ActiveThreat.objects.default_metrics(pk=skill.pk).first() # получим аннотированную метрику
skill.content_count # выдаёт 1
skill.content.count() # всё так же выдаёт 2
Почему так происходит и как это решить, не прибегая к raw sql?