我有一些定期进行调查的用户。该系统具有多个调查,从指定类型的上次发布调查的提交日期起,将以一定的时间间隔发布该调查。
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模型进行查询时不起作用?
答案 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,但是我还没有找到一种方法,因为您无法使用Prefetch
在ArrayField
中。