Аннотируйте индивидуально каждый элемент в наборе запросов Django Queryset
Я пытаюсь получить queryset, который говорит мне, подписан ли пользователь на данный список рассылки, и получить доступ к этому bool с помощью mailing_list.is_subbed
В идеале мы должны иметь набор запросов, где каждый элемент имеет аннотированное поле "is_subbed", которое равно True или False в зависимости от того, подписан ли данный пользователь на список рассылки или нет.
Для контекста, это представление будет обслуживать форму с чекбоксами, которые отмечаются/отмечаются в зависимости от статуса пользователя. Страница доступна в режиме инкогнито через url, который содержит токен, содержащий 1) email пользователя и 2) ID записи отправки почты (которая содержит детали, такие как список рассылки, в который она была отправлена, подробности ниже)
> В текущем состоянии функция is_subbed вызывается только один раз на первом элементе, и результирующий bool аннотируется к каждому элементу, я бы хотел, чтобы она выполнялась для каждого элемента в queryset.
Как я могу это сделать? На данный момент, если первый элемент возвращает True после подачи на is_subbed, каждый флажок будет отмечен, потому что аннотированное поле is_subbed устанавливается в True для каждого элемента.
Вот моя текущая работа: Реферат :
- The view
- Implementation of function and models used in said view
- Snippet I use to access my results in jinja2
views.py
class PressSubscriptionManagementView(TemplateView):
"""Allows management of Press subscriptions
Displays the current subscriptions to the different press Mailing lists
and allows the user to sub / unsub from a checkbox form.
Press mailing list are those whose attribute mailing_list_type is "PR"
"""
template_name = "mailing_list/press_subscription_management.html"
def is_subbed(self, user: User, mailing_list: MailingList) -> bool:
"""
Check if the user is subbed to the mailing list
"""
return user_current_state(user, mailing_list).event_type == "sub"
def get_context_data(self, **kwargs) -> dict:
context = super().get_context_data(**kwargs)
email, send_record_id = token_valid(kwargs["token"])
if email and send_record_id:
context["user"] = User.objects.get(email=email)
# In the current state, is_subbed is only called once on the first
# item in the list. If this call returns True, every checkbox is
# checked. None otherwise.
context["press_subscription_list"] = \
MailingList.objects.filter(mailing_list_type="PR").order_by(
"-id").annotate(
is_subbed=ExpressionWrapper(
Value(
self.is_subbed(
context["user"],
F('mailing_list__id')
),
output_field=BooleanField()),
output_field=BooleanField()
)
)
else:
context["press_subscription_list"] = \
MailingList.objects.filter(
mailing_list_type="PR").order_by("-id")
return context
реализация user_current_state :
def user_current_state(user, mailing_list):
"""Return user's most current state on the provided mailing list
Return the most recent event associated with this user in this
mailing list.
"""
try:
the_user = MailingListEvent.objects.filter(
Q(event_type=MailingListEvent.EventType.SUBSCRIBE) |
Q(event_type=MailingListEvent.EventType.UNSUBSCRIBE),
user=user, mailing_list=mailing_list).latest(
'event_timestamp')
return the_user
except MailingListEvent.DoesNotExist:
return MailingListEvent(
user=user, mailing_list=mailing_list,
event_type=MailingListEvent.EventType.UNSUBSCRIBE)
Внедрение MailingList и MailingListEvent :
class MailingListEvent(models.Model):
"""Events on mailing lists.
This represents subscribes, unsubscribes, and bounces. We'd like
to understand what happens and when, not just the current state of
the system.
"""
class EventType(models.TextChoices):
SUBSCRIBE = 'sub', 'inscription'
UNSUBSCRIBE = 'unsub', 'désinscription'
BOUNCE = 'bounce', 'bounce'
user = models.ForeignKey(User, on_delete=models.CASCADE)
mailing_list = models.ForeignKey(MailingList,
on_delete=models.CASCADE)
event_timestamp = models.DateTimeField(default=django.utils.timezone.now)
event_type = models.CharField(max_length=6, choices=EventType.choices)
class MailingList(models.Model):
"""Represent things a mailing list.
"""
class Meta:
permissions = (
("may_view_list",
"May see list of mailing lists and their metrics"),
)
# This is a user-visible name that can change.
mailing_list_name = models.CharField(max_length=80, blank=False)
# This is the unique name that can't change. It just makes code a
# bit more readable than referring to the pk id.
mailing_list_token = models.CharField(max_length=80, blank=False,
unique=True)
# How often the user should expect to be contacted.
# Zero means no frequency objective.
contact_frequency_weeks = models.IntegerField(default=0)
list_created = models.DateTimeField(default=django.utils.timezone.now)
list_active = models.BooleanField(default=False)
is_petition = models.BooleanField(default=False)
И наконец, реализация SendRecord :
class TopicBlogEmailSendRecord(models.Model):
"""Represent the fact that we sent an email.
"""
slug = models.SlugField(max_length=90, allow_unicode=True, blank=True)
mailinglist = models.ForeignKey(MailingList, on_delete=models.PROTECT)
recipient = models.ForeignKey(User, on_delete=models.PROTECT)
send_time = models.DateTimeField(auto_now=True)
open_time = models.DateTimeField(null=True, blank=True)
click_time = models.DateTimeField(null=True, blank=True)
unsubscribe_time = models.DateTimeField(null=True, blank=True)
В шаблоне я получаю доступ к значению следующим образом :
{% for mailing_list in press_subscription_list %}
[...]
<input type="checkbox" name="{{ mailing_list.mailing_list_name }}"
value="{{ mailing_list.id }}" id="mailing_list_id_{{ mailing_list.id }}"
{% if mailing_list.is_subbed %} checked {% endif %} />
[...]
{% endfor %}
Вместо вашего метода is_subbed() можно использовать подзапрос в сочетании с условным выражением.
Например, что-то вроде этого:
def get_context_data(self, **kwargs) -> dict:
...
# subquery mailing list events for each of the user's mailing lists
events = MailingListEvent.objects.filter(
user=context['user'], mailing_list=models.OuterRef('pk')
).order_by('-event_timestamp')
# annotate using subquery
annotated_mailing_lists = MailingList.objects.annotate(
latest_event_type=models.Subquery(events.values('event_type')[:1]),
is_subbed=models.Case(
models.When(latest_event_type='sub', then=True),
default=False,
output_field=models.BooleanField(),
)
)
context["press_subscription_list"] = annotated_mailing_lists.filter(...)...
Примечания:
- Это предполагает, что ваша модель
MailingListEventопределяет поляevent_typeиevent_timestamp(которые отсутствуют в вашем примере). - Для ясности я использовал
When(latest_event_type='sub', ..., но вы, вероятно, должны использоватьMailingListEvent.EventType.SUBSCRIBEвместо'sub'.