django使用queryset注释

时间:2019-08-26 15:27:37

标签: 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...

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

A.interval + SurveyRun.objects.filter(
    user=user, 
    survey=A
).latest('submitted').submitted

我想执行一项日常任务,该任务查询所有用户并根据此条件为所有进行调查的用户创建新运行:

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

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

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

users_with_surveys_due = User.objects.with_surveys_due().filter(surveys_due__isnull=False)

其中带注释的字段将是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)

但是,我希望在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)

更新#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']>

用户对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

更新#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)

更新#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)
        )

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

result.filter(
    Q(has_survey_runs=False) |
        (
           Q(has_pending_runs=False) &
           Q(latest_submission_date__lte=today - F('interval'))
        )
    )

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

1 个答案:

答案 0 :(得分:4)

要注释用户是否有到期调查,我建议使用Subquery expression

.only-show-on-hover {
  display: none;
  position: fixed;
  width: 206px; 
  height: 100px; 
  border: 1px solid black;
}

我仍在尝试找出另一种方法。您不能用另一个查询集注释一个查询集,值必须与字段等效。另外,很遗憾,您不能将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) 用作Subquery的{​​{1}}参数。但是,由于您使用的是PostgreSQL,因此可以使用queryset来以包装值列出调查的ID,但是我还没有找到一种方法,因为您无法使用PrefetchArrayField中。