django用queryset注释

Ver*_*int 6 django django-models

我有定期进行调查的用户。该系统具有多个调查,从指定日期的最后一次发布的特定类型调查的提交日期起,它会按设定的时间间隔发布。

class Survey(Model):
    name = CharField()
    description = TextField()
    interval = DurationField()  
    users = ManyToManyField(User, related_name='registered_surveys')
    ...

class SurveyRun(Model):
    ''' A users answers for 1 taken survey '''
    user = ForeignKey(User, related_name='runs')
    survey = ForeignKey(Survey, related_name='runs')
    created = models.DateTimeField(auto_now_add=True)
    submitted = models.DateTimeField(null=True, blank=True)
    # answers = ReverseForeignKey...
Run Code Online (Sandbox Code Playgroud)

因此,使用上述模型时,应提醒用户A该日期下一次进行调查:

A.interval + SurveyRun.objects.filter(
    user=user, 
    survey=A
).latest('submitted').submitted
Run Code Online (Sandbox Code Playgroud)

我想运行一个每日定期任务,该任务查询所有用户并根据以下条件为所有进行调查的用户创建新运行:

对于每个调查,都向用户注册:

  • 如果该用户调查组合没有运行,则为该用户调查组合创建第一个运行并警告用户
  • 如果该调查有运行,但没有一个运行(已创建一个开放运行但尚未提交submitted=None),并且最近一次的提交日期加上该调查的间隔为<=今天,请为该用户调查组合创建一个新运行并发出警报用户

理想情况下,我可以创建一个管理器方法,该方法使用以下surveys_due字段进行注释:

users_with_surveys_due = User.objects.with_surveys_due().filter(surveys_due__isnull=False)
Run Code Online (Sandbox Code Playgroud)

其中带注释的字段将是Survey对象的查询集,用户需要为其提交新一轮的答案。我可以发出这样的警报:

for user in users_with_surveys_due.all():
    for survey in user.surveys_due:
        new_run = SurveyRun.objects.create(
            user=user,
            survey=survey
        )
        alert_user(user, run)
Run Code Online (Sandbox Code Playgroud)

但是,我将为User对象提供一个布尔标志注释,以指示registered_surveys创建新运行的需求之一。

我将如何实施类似此with_surveys_due()管理器方法的方法,以便Postgres承担所有繁重的工作?是否可以使用集合对象(例如反向FK)进行注释?

更新:

为了清楚起见,这是我当前在python中的任务:

def make_new_runs_and_alert_users():
    runs = []
    Srun = apps.get_model('surveys', 'SurveyRun')
    for user in get_user_model().objects.prefetch_related('registered_surveys', 'runs').all():
        for srvy in user.registered_surveys.all():
            runs_for_srvy = user.runs.filter(survey=srvy)
            # no runs exist for this registered survey, create first run
            if not runs_for_srvy.exists():
                runs.append(Srun(user=user, survey=srvy))
                ...

            # check this survey has no open runs
            elif not runs_for_srvy.filter(submitted=None).exists():
                latest = runs_for_srvy.latest('submitted')
                if (latest.submitted + qnr.interval) <= timezone.now():
                    runs.append(Srun(user=user, survey=srvy))
    Srun.objects.bulk_create(runs)
Run Code Online (Sandbox Code Playgroud)

更新#2:

在尝试使用Dirk解决方案时,我有一个简单的示例:

In [1]: test_user.runs.values_list('survey__name', 'submitted')                                                                                                                                     
Out[1]: <SurveyRunQuerySet [('Test', None)]>
In [2]: test_user.registered_surveys.values_list('name', flat=True)                                                                                                                                 
Out[2]: <SurveyQuerySet ['Test']>
Run Code Online (Sandbox Code Playgroud)

用户有一个submitted=None用于Test调查的公开运行(),并已注册到一个调查(Test)。不应将他/她标记为新的跑步,因为对于他/她注册的唯一问卷而言,有未提交的未完成跑步。因此,我创建了一个封装Dirk解决方案的函数get_users_with_runs_due:

In [10]: get_users_with_runs_due()                                                                                                                                                                  
Out[10]: <UserQuerySet [<User: test@gmail.com>]> . # <-- should be an empty queryset

In [107]: for user in _: 
              print(user.email, i.has_survey_due)  
test@gmail.com True  # <-- should be false
Run Code Online (Sandbox Code Playgroud)

更新#3:

在我以前的更新中,我对逻辑进行了一些更改,以正确匹配我想要的内容,但忽略了提及或显示更改。这是下面的查询功能,其中包含更改的注释:

def get_users_with_runs_due():
    today = timezone.now()

    survey_runs = SurveyRun.objects.filter(
        survey=OuterRef('pk'),
        user=OuterRef(OuterRef('pk'))
    ).order_by('-submitted')

    pending_survey_runs = survey_runs.filter(submitted__isnull=True)

    surveys = Survey.objects.filter(
        users=OuterRef('pk')
    ).annotate(
        latest_submission_date=Subquery(
            survey_runs.filter(submitted__isnull=False).values('submitted')[:1]
        )
    ).annotate(
        has_survey_runs=Exists(survey_runs)
    ).annotate(
        has_pending_runs=Exists(pending_survey_runs)
    ).filter(
        Q(has_survey_runs=False) | # either has no runs for this survey or
        ( # has no pending runs and submission date meets criteria
            Q(has_pending_runs=False, latest_submission_date__lte=today - F('interval'))
        )
    )

    return User.objects.annotate(has_survey_due=Exists(surveys)).filter(has_survey_due=True)
Run Code Online (Sandbox Code Playgroud)

更新#4:

我试图通过创建一个函数来隔离问题,该函数将按用户对“调查”进行大部分注释,以尝试在使用该模型查询User模型之前检查该级别的注释。

def annotate_surveys_for_user(user):
    today = timezone.now()

    survey_runs = SurveyRun.objects.filter(
        survey=OuterRef('pk'),
        user=user
    ).order_by('-submitted')

    pending_survey_runs = survey_runs.filter(submitted=None)

    return Survey.objects.filter(
            users=user
        ).annotate(
            latest_submission_date=Subquery(
                survey_runs.filter(submitted__isnull=False).values('submitted')[:1]
            )
        ).annotate(
            has_survey_runs=Exists(survey_runs)
        ).annotate(
            has_pending_runs=Exists(pending_survey_runs)
        )
Run Code Online (Sandbox Code Playgroud)

这按预期工作。注释正确的地方并使用以下方法进行过滤:

result.filter(
    Q(has_survey_runs=False) |
        (
           Q(has_pending_runs=False) &
           Q(latest_submission_date__lte=today - F('interval'))
        )
    )
Run Code Online (Sandbox Code Playgroud)

产生了预期的结果:一个空的查询集,其中用户不应进行任何运行,反之亦然。在将其作为子查询并从User模型进行查询时,为什么这不起作用?

dir*_*ten 10

要注释用户是否有到期调查,我建议使用子查询表达式:

from django.db.models import Q, F, OuterRef, Subquery, Exists
from django.utils import timezone

today = timezone.now()

survey_runs = SurveyRun.objects.filter(survey=OuterRef('pk'), user=OuterRef(OuterRef('pk'))).order_by('-submitted')

pending_survey_runs = survey_runs.filter(submitted__isnull=True)

surveys = Survey.objects.filter(users=OuterRef('pk'))
   .annotate(latest_submission_date=Subquery(survey_runs.filter(submitted__isnull=False).values('submitted')[:1]))
   .annotate(has_survey_runs=Exists(survey_runs))
   .annotate(has_pending_runs=Exists(pending_survey_runs))
   .filter(Q(has_survey_runs=False) | Q(latest_submission_date__lte=today - F('interval')) & Q(has_pending_runs=False))

User.objects.annotate(has_survey_due=Exists(surveys))
    .filter(has_survey_due=True)
Run Code Online (Sandbox Code Playgroud)

我仍在努力弄清楚如何做另一个。您不能用另一个查询集注释一个查询集,值必须是字段等效项。不幸的是,您也不能使用Subqueryasqueryset参数Prefetch。但是由于您使用的是 PostgreSQL,因此您可以使用ArrayField在包装值中列出调查的 ID,但我还没有找到一种方法来做到这一点,因为您不能aggregate在Subquery.

  • 晚上剩下的时间不会上网。明天回来。也许你从我的回答中得到了一些想法,然后自己解决它。 (2认同)