Annotate individually each item in a Django Queryset - python

Objective
I'm trying to get a queryset that tells me if a user is subscribed to a given mailing list and being able to access that bool using mailing_list.is_subbed
Ideally we would have a queryset where each item has a annotated field "is_subbed" which is either True or False.
Context
For context, this view is going to serve a form with checkboxes that are checked/unchecked depending on the status of the user.
The page is accessible in incognito mode through a url that holds a token which contains 1) The email of the user and 2) The ID of the mail send record (which holds details like the mailing list it's been sent to, details below)
Question
In the current state, the is_subbed function is called only once on the first item, and the resulting bool is annotated to every item, I'd like it to run for each item in the queryset.
How can I do that ? For now if the first item returns True once fed to is_subbed, every checkbox is marked because the annotated field is_subbed is set to True on each item.
Code
Here is my current work :
Summary :
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):
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"] = \
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( # noqa
is_subbed=(
ExpressionWrapper(
Value(
self.is_subbed(
context["user"],
F('mailing_list__id')
),
output_field=BooleanField()
),
output_field=BooleanField()
)
)
)
return context
implementation of 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)
Implementation of MailingList and MailingListEvent :
class MailingListEvent(models.Model):
class EventType(models.TextChoices):
SUBSCRIBE = 'sub', 'inscription'
UNSUBSCRIBE = 'unsub', 'désinscription'
user = models.ForeignKey(User, on_delete=models.CASCADE)
mailing_list = models.ForeignKey(MailingList,
on_delete=models.CASCADE)
class MailingList(models.Model):
# 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)
And Finally the implementation of SendRecord :
class TopicBlogEmailSendRecord(models.Model):
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)
In the template, I access the value like this :
{% 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 %}

Instead of your is_subbed() method, you could use a Subquery, combined with a conditional expression.
For example, something like this:
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(...)...
Notes:
This assumes your MailingListEvent model defines event_type and event_timestamp fields (which are missing from your example).
For clarity, I used When(latest_event_type='sub', ..., but you should probably use MailingListEvent.EventType.SUBSCRIBE instead of 'sub'.

Related

How to make an intuitive form field for a M2M relationship with many options in Django?

Apologies if the title is poorly worded, I couldn't figure out a succinct way to describe this issue.
I've been working with a client who uses the Django form library to allow users to create/edit model instances. I've recently added a many to many relationship between model A and model B, and I want there to be an input on the model A form that allows users to connect instances of model B to it. The SelectMultiple widget used by Django by default is unintuitive, and the alternative CheckboxSelectMultiple widget seems like it wouldn't be great once there are 100s of options (which is a likely scenario). What complicates this further is that there's also a M2M relationship between model C and D, connected by a through table that determines a D instance's order on a C instance, and neither of those widgets work well with through tables.
My initial thought was to use CheckboxSelectMultiple and just order the already selected options at the top, wrapping the widget in a scroll overflow container. I unfortunately don't know how to order options in that way though, since the accepted queryset parameter is inherently unordered. For the case with the through table, ideally each checked option would have an input next to it where users could set the order of the respective instance, but I'm not sure how to fundamentally alter the widget in that way.
So, how would you advise I go about making a form field for a M2M relationship with many options? Is there a way to make my modified CheckboxSelectMultiple idea work, or is there an alternative, more efficient way to do this (possibly involving libraries)? I can't imagine I'm the first person to encounter this problem.
Here's "one I wrote earlier" Sorry, this is a bit of an info-dump. Has worked, but not fully tested. Check that it's csrf-safe if you care about that (it doesn't use a form, just picks things out of request.POST)
class GenericEditM2MView( DetailView):
#model = Model # required as per DetailView
# template_name = # as per DetailView
#m2m_fieldname = None # no longer required if unique: the name of the model's m2m field to operate on
remove_qs = None # defaults to .all() of the m2m field
success_url = '.' # defaults to displaying the modified m2m relationship unless done
done_url = None # where to go if submit="Done", defaults to success_url
#
"""template_name must define a form full of checkboxes, obtained from
{% for p in add_qs %}
<input type="checkbox" name="add" value="{{p.pk}}" > {% endfor}
{% for p in remove_qs %}
<input type="checkbox" name="remove " value="{{p.pk}}" > {% endfor}
default is to return to this same view after submit, to show that the changes
have been made. You can supply <input type="submit" name="submit" value="done" />
which will go to done_url instead of success_url
example use:
class PenStockM2MView( GenericEditM2MView):
template_name = 'playpen/edit_m2m.html'
model = PenStock
# m2m_fieldname = 'name' # works it out for itself if ony one M2M field on the model
done_url = '/playpen/OK'
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
# everything works without this __init__ provided self.m2m_fieldname is present and correct.
# if model has only one m2m field, locate it via _meta as default.
# Also check m2m_fieldname is m2m because very confusing errors later if it's not!
f = getattr(self, 'm2m_fieldname', None)
m2m_fieldnames = [
field.name for field in self.model._meta.get_fields() if field.many_to_many ]
model_name = self.model.__name__
if f and not f in m2m_fieldnames:
raise AttributeError( f'field "{f}" is not a many-to-many field in {model_name}')
if not f:
if len( m2m_fieldnames ) == 1:
self.m2m_fieldname = m2m_fieldnames[0]
else:
raise AttributeError( f'Cannot identify a unique many-to-many field in {model_name}' )
def get_add_queryset(self):
field = getattr( self.object, self.m2m_fieldname)
remove_qs = self.get_remove_queryset()
already_there = remove_qs.values_list('pk', flat=True)
return remove_qs.model.objects.exclude( pk__in = already_there) # is qs.model documented?
def get_remove_queryset(self):
if hasattr( self, 'remove_queryset'):
return self.remove_qs
remove_qs = getattr( self.object, self.m2m_fieldname)
return remove_qs.all()
def get_context_data( self, **kwargs):
context = super().get_context_data( **kwargs)
context['add_qs'] = self.get_add_queryset()
context['remove_qs'] = self.get_remove_queryset()
return context
def post( self, request, *args, **kwargs):
self.object = self.get_object()
add = request.POST.getlist('add')
remove = request.POST.getlist('remove')
add_objs = list( self.get_add_queryset().filter(pk__in=add) )
remove_objs = list( self.get_remove_queryset().filter(pk__in=remove) )
field = getattr( self.object, self.m2m_fieldname )
field.add( *add_objs)
field.remove( *remove_objs)
return HttpResponseRedirect( self.get_done_url() or self.get_success_url() )
def get_success_url(self):
return self.success_url
def get_done_url( self):
done = self.request.POST.get("submit", None)
if done == "done" and hasattr(self, 'done_url'):
return self.done_url
return None

Django: Calling a model's function in my template - not working

I'm trying to call a function from my model (check_nick) in my template. It appears that the template is successfully hitting the function since the items in the function are printed. However I'm not getting the expected result (True) as the user.group I'm testing with is NICK which is part of the NICK_BRANDS list.
MODEL.PY:
NICK_BRANDS = ['NICK', 'NICKT', 'NICKN', 'NICKK', 'NICKA']
class User():
group = models.ForeignKey(Brand, null=True, blank=True)
def check_nick(self):
brand = self.group
print brand //prints NICK
print brand in NICK_BRANDS //prints False (should be True!)
if brand in NICK_BRANDS:
return True
else:
return False
TEMPLATE:
{% if user.check_nick %}
//add some markup
{% endif %}
Your debug prints some string representation of brand, but you are checking the actual object. Change your if-clause to sth like:
if str(brand) in NICK_BRANDS:
# if brand.title in NICK_BRANDS:
# if brand.name in NICK_BRANDS:
# or whatever field of Brand is "NICK"
self.group will be an instance of the related Brand model, not a string, and hence would probably not return True with the in statement. I presume there is some Brand.name property and you should be using:
def check_nick(self):
return self.group.name in NICK_BRANDS

How to make foreign key accept field value instead of its id in django?

I have created a checkbox for content filtering of products based on category.So when the user clicks on any checkbox only the books with that category should be shown.In the view I am passing the value of checkbox field(category name) obtained from the template but upon filtering, the foreign key is expecting pk(id) instead of field value.I am getting error like this,invalid literal for int() with base 10: '<category name>'.So is it possible to make foreign key accept value instead of id?
Models.py,
class Add_cat(models.Model):
category = models.CharField("Name",max_length=25,unique=True)
def __unicode__(self):
return u'{0}'.format(self.category)
class Add_prod(models.Model):
book = models.CharField("Book Name",max_length=40)
author = models.CharField("Author",max_length=30)
price = models.PositiveIntegerField("Price")
image = models.ImageField(upload_to='images',null=True)
cat = models.ForeignKey(Add_cat,on_delete=models.PROTECT)
Template file,
{% for i in products %}
<input type="checkbox" name="cat_name" value="{{i.cat}}">{{i.cat}}<br>
{% endfor %}
Views.py,
def welcome_user(request):
if 'cat_name' in request.GET:
filter_category = request.GET.get('cat_name')
my_products = Add_prod.objects.filter(cat__in = filter_category)
context = { "products":my_products}
else:
my_products = Add_prod.objects.all()
context = { "products":my_products}
return render(request,"welcome-user.html",context)
You can check in the category field itself:
my_products = Add_prod.objects.filter(cat__category__in=filter_category)
Have a look at the documentation on how this works.
Above, is only applicable if filter_category is a list. If it is a string you can filter like following:
my_products = Add_prod.objects.filter(cat__category=filter_category)
There are two things wrong with your code
You need to look up the field rather than the foreign key
By using __in you are looking the category is equal to any one of the characters in the filter_category.
Hence to fix, use the field lookup and remove the __in
Add_prod.objects.filter(cat__category=filter_category)
You can try this,it will help you:
Add_prod.objects.filter(cat__category = filter_category)

Python Django return unique save dates and filter model objects

I have two models
Room and RoomLog
each single Room object can have multiple RoomLog objects.
the purpose of it is that Room object has got an attribute: value and method save(). I create a new related RoomLog object every time a user saves changed attribute of any specific Room object and saves it.
RoomLog object has got value attribute and date attribute.
date is related to Room save method so it gives Room changed value save DATE.
My question is:
Q1: How to return all unique days from all RoomLog objects so I know when any save took place ?
Q2: Let's say we know how to return unique days. So, the question is: how to select any day from these unique days and display all Room objects values by chosen date ? I would like to return last saved value from chosen date for each Room object.
The way I tackle both questions at the moment (I am looking for more Pythonic, faster, better performing solutions) is:
I created Form in which I iterate through RoomLog objects:
class MyForm(forms.Form):
roomdates = []
roomextracted = []
for r in RoomLog.objects.all():
if r not in roomdates:
roomdates.append(r.update_date)
for i in roomdates:
if i not in roomextracted:
roomextracted.append(i)
ROOMDATA = [(r, r) for r in roomextracted]
my_choice_field = forms.ChoiceField(choices=ROOMDATA)
then I have a view to pass selected date to another view in which I filter Room.objects.all() by selected date:
def choices(request):
form = RoomLogChoices()
form.fields['choice'].choices = list()
testlist = []
for rl in RoomLog.objects.all():
if rl.update_date not in testlist:
testlist.append(rl.update_date)
for d in testlist:
form.fields['choice'].choices.append(
(
d,d
)
)
return render(request, 'prostats/choices.html', {'form':form})
next I have choicesform.html in which I select date from dropdown menu:
{% extends "base.html" %}
{% block content %}
<form action="" method="post" >
{% csrf_token %}
<ul>
{% for choice in form.my_choice_field.field.choices %}
<li>
<input type="radio" name="my_choice_field" value="{{choice.0}}"
{% if equal form.my_choice_field.data choice.0 %}
checked="checked"
{% endifequal %}/>
<label for="">{{choice.1}}</label>
</li>
{% endfor %}
</ul>
<input type="submit" value="Submit" />
</form>
{% endblock %}
and this is the view in which I handle POST data
class AllRoomsView(ListView):
template_name = 'prostats/roomsdetail.html'
queryset = Room.objects.all()
def get_context_data(self, **kwargs):
context = super(AllRoomsView, self).get_context_data(**kwargs)
context['rooms'] = Room.objects.all()
context['rlog'] = RoomLog.objects.all()
roomsdates = []
for r in context['rlog']:
if r not in roomsdates:
roomsdates.append(r.update_date)
roomextracted = []
for i in roomsdates:
if i not in roomextracted:
roomextracted.append(i)
context['roomextracted'] = roomextracted
choicedate = self.request.GET.get('choice')
if choicedate != None:
choosend = choicedate
else:
choosend = '2016-02-01'
#filtered rlogs
rlfilteredempty = []
for r in context['rooms']:
i = RoomLog.objects.filter(room=r.id, update_date__lte = choosend).order_by('-update_date')[:1]
if i:
rlfilteredempty.append(i[0])
else:
rlfilteredempty.append(r)
context['rlfiltered'] = rlfilteredempty
context['choicedate'] = self.request.GET.get('choice')
#context['roomfiltersettime'] = RoomLog.objects.filter(update_date__lte = choosend)
context['roomfiltersettime'] = RoomLog.objects.filter(update_date__lte = choosend)
rslice = []
for r in context['rooms']:
i = RoomLog.objects.filter(room=r.id, update_date__lte = choosend).order_by('-update_date')[:1]
if i:
for rsobject in i:
rs = (r.flat.flat_number,r.flat.block.block_name,r.room_name)
rl = rsobject.id
rv = rsobject.room_value
rslice.append((rs,rl,rv))
else:
rs = (r.flat.flat_number,r.flat.block.block_name,r.room_name)
r = r.id
rslice.append((rs,r))
context['rslice'] = rslice
So, all of it what I have done I feel is not very good and maybe somebody can point me with some good ideas on how to tackle this problem better ?
EDIT: update of the post with my Room and RoomLog models:
class Room(models.Model):
room_name = models.CharField(max_length= 10)
room_value = models.PositiveSmallIntegerField(default=0)
flat = models.ForeignKey(Flat)
created_date = models.DateField(auto_now_add= True)
created_time = models.TimeField(auto_now_add= True)
substage_name = models.CharField(max_length=50,default="")
def __init__(self, *args, **kwargs):
super(Room, self).__init__(*args, **kwargs)
self.value_original = self.room_value
def save(self, **kwargs):
with transaction.atomic():
response = super(Room, self).save(**kwargs)
if self.value_original != self.room_value:
room_log = RoomLog()
room_log.room = self
room_log.room_value = self.value_original
room_log.save()
return response
class RoomLog(models.Model):
room = models.ForeignKey(Room)
room_value = models.PositiveSmallIntegerField(default=0)
update_date = models.DateField(auto_now_add= True)
update_time = models.TimeField(auto_now_add= True)
To return all unique days, use distinct() on your created_date field. That will, of course, only work if created_date is actually a date and not a datetime value!
RoomLog.objects.all().distinct('created_date')
If your created value is a datetime, you need to make it a date first, using Django's func() and F() functions. That uses the DATE() SQL functions that may not work on all databases, but it does on Postgres and probably many others.
RoomLog.objects.all()\
.annotate(created_date=Func(F('created'), function='DATE'))\
.order_by('-created_date')\
.distinct('created_date')
Only a partial answer. The second question depends on the layout of your models that you didn't post.

2 different search in a view

I have two tables one that stores information from a medical record and the other a second medical consultation, what I want is to make a search and if the patient had a second medical consultation shows me the information of the second medical consultation in my template, if the patient doesn't have a second medical consultation only show me the information of the medical record.
I need help and ideas on how I can do it, I'm new with Django and Python , I do the search in a view in the following way, the problem with this search is that I only see the medical record and I need that if the patient had second medical consultation to display the information.
View.py
def Expediente_Detalle(request, credencial):
Expediente_Detalle = get_object_or_404(ExpedienteConsultaInicial, credencial_consultainicial=credencial)
return render(request, 'ExpedienteDetalle.html', {'Expediente_Detalle': Expediente_Detalle})
Models.py
class ExpedienteConsultaInicial(models.Model):
credencial_consultainicial = models.CharField(max_length=10, null=True, blank=True)
def __unicode__(self):
return self.credencial_consultainicial
class ConsultasSubsecuentes(models.Model):
Consultasbc_credencial =models.ForeignKey(ExpedienteConsultaInicial,
related_name='csb_credencial')
Try:
#Models
class ExpedienteConsultaInicial(models.Model):
#max_legth=10 might be too small
credencial_consultainicial = models.CharField(max_length=100, null=True, blank=True)
def __unicode__(self):
return self.credencial_consultainicial
class ConsultasSubsecuentes(models.Model):
#related_name is name of attribute of instance of model
#to (not from!) which ForeignKey points.
#Like:
#assuming that `related_name` is 'consultations'
#instance = ExpedienteConsultaInicial.objects.get(
# credencial_consultainicial='text text text'
#)
#instaqnce.consultations.all()
#So I suggest to change `related_name` to something that will explain what data of this model means in context of model to which it points with foreign key.
consultasbc_credencial = models.ForeignKey(ExpedienteConsultaInicial,
related_name='consultations')
#View
def expediente_detalle(request, credencial):
#Another suggestion is to not abuse CamelCase - look for PEP8
#It is Python's code-style guide.
detalle = get_object_or_404(ExpedienteConsultaInicial, credencial_consultainicial=credencial)
subsequent_consultations = detalle.csb_credencial.all()
return render(request, 'ExpedienteDetalle.html', {'Expediente_Detalle': detalle, 'consultations': subsequent_consultations})
Useful links:
Following relationships backward - thats why I suggest you to
change related_name
PEP8 - and this is about CamelCase and
Python's code-style.
All you need to do is to execute another query, and provide the result to your template
def Expediente_Detalle(request, credencial):
Expediente_Detalle = get_object_or_404(ExpedienteConsultaInicial, credencial_consultainicial=credencial)
second_consultation = ExpedienteConsultaInicial.objects.filter(..)
return render(request, 'ExpedienteDetalle.html', {'Expediente_Detalle': Expediente_Detalle, 'second_consultation': second_consultation})
Then, in your template, iterate over second_consultation:
{% for c in second_consultation %}
<p> {{ c.credencial_consultainicial }} </p>
{% endfor %}

Categories

Resources