How to automatically update model fields in Django? - python

So I have this simple models:
class Room(models.Model):
STATUS = (
('Available', 'Available'),
('Occupied', 'Occupied'),
)
status = models.CharField(choices=STATUS)
name = models.CharField(max_length=200)
class Reservation(models.Model):
STATUS = (
('Confirmed', 'Confirmed'),
('Pending', 'Pending')
)
status = models.CharField(choices=STATUS)
room = models.OneToOneField(Room, on_delete=)
date_created = models.DateTimeField(auto_now_add=True)
I want that whenever I create a new Reservation and assign a room to it, the status field of that particular Room is automatically changed to 'Occupied'.
I think there is a way to do this with Django Signals but I haven't figured out how to implement it on my own yet.
Thanks in advance.
Also, if you think there is a better way to implement said functionality by simplifying or modifying said models please feel free to post it
views:
def room(request):
rooms = Room.objects.all()
context = {'rooms': rooms}
return render(request, 'hotel_app/room.html', context)
def reservation(request):
reservations = Reservation.objects.all()
context = {'reservations': reservations}
return render(request, 'hotel_app/reservations.html', context)

I think there is a way to do this with Django Signals but I haven't figured out how to implement it on my own yet.
Please don't use signals. Signals are an anti-pattern. Except for certain cases, it will often result in more trouble. Indeed, signals do not run on a lot of ORM calls, especially ones where you create/remove/upate in bulk. It furthermore makes saving and updating objects less predictable.
You can simply use .annotate(…) [Django-doc] to determine if there are reservations for a given room:
from django.db.models import Exists, OuterRef
Room.objects.annotate(
is_occupied=Exists(
Reservation.objects.filter(room=OuterRef('pk'))
)
)
The Room objects that arise from this queryset will have an extra attribute .is_occupied that is True if a Reservation for that Room exists, and False otherwise.
If you need this often, you can define a manager for the Room model that will automatically annotate:
class RoomManager(models.Model):
def get_queryset(self, *args, **kwargs):
super().get_queryset(*args, **kwargs).annotate(
is_occupied=Exists(
Reservation.objects.filter(room=OuterRef('pk'))
)
)
class Room(models.Model):
name = models.CharField(max_length=200)
objects = RoomManager()
each time you thus access Room.objects, you get a manager that will return a queryset that is annotated.

Related

DJango rest framework - API list using filter field from related models

Hi I'm new to Django and the Django rest framework so my terminology may be off.
I'm trying to build an API that gives back a list of items from a model but filtered based on fields in another related model.
I'll provide my current view and serializer classes and models
class service(models.Model):
name = models.CharField(max_length=50)
vendor = models.CharField(max_length=50)
version = models.CharField(max_length=10)
registration_status = models.BooleanField(default=False)
class service_network(models.Model):
service = models.OneToOneField(
service,
related_name='network',
on_delete=models.CASCADE,
primary_key=True,
)
forwarded_port = models.CharField(max_length=50)
class ServiceNetworkSerializer(serializers.ModelSerializer):
class Meta:
model = service_network
fields = '__all__'
class ServiceSerializer(serializers.ModelSerializer):
network = ServiceNetworkSerializer()
class Meta:
model = service
fields = [
'id',
'name',
'vendor',
'version',
'registration_status',
'network',
]
class ServiceAPI(ModelViewSet):
queryset = service.objects.all()
serializer_class = ServiceSerializer
filterset_fields = '__all__'
Currently I can get back lists using a URL query string
{{baseUrl}}/engine/service?registration_status=true
What I want to do is something like this
{{baseUrl}}/engine/service/network?forwarded_port=8080
Which I would expect to give back a list of services where the related network field "forwarded_port" is equal to 8080.
Is there another way to query this API? Maybe using a POST with a body containing the query? If there something in the DOCS that I can read, I've tried to look through filtering and querysets but I wasn't able to find anything that would do this out of the box
I'm also new to stackoverflow and I've tried to keep my question short with as much relevant information so if there anything missing I'd be happy to edit my question
I was able to solve this using the following queryset override
def get_queryset(self):
if len(self.request.GET) > 0:
query_set = {}
for query in self.request.GET:
query_set[query] = self.request.GET.get(query)
return service.objects.filter(**query_set)
else:
return service.objects.all()
What this does is lets you filter fields without explicitly specifying what they are, in cases when you have many fields that need filtering. I also have to say as I'm not experienced with Django, I'm not sure what kind of errors this may bring up but its a hack that's worked for me. If I find this is really bad I'll come back and remove this.
Try this:
{{baseUrl}}/engine/service?network__forwarded_port=8080
Probably it works.
Doc: https://docs.djangoproject.com/en/dev/topics/db/queries/#lookups-that-span-relationships-1
EDIT:
If the above answer doesn't work, you can change the ServiceApi class and filter by yourself:
class ServiceAPI(ModelViewSet):
def get_queryset(self):
if self.request.GET.get(network__forwarded_port)
return service.objects.filter(network__forwarded_port = self.request.GET.get(network__forwarded_port))
else:
return service.objects.all()
serializer_class = ServiceSerializer
filterset_fields = '__all__'

Partial updating a ManyToMany field, but keeping its get representation

I've been scratching my head about this problem for a couple of hours now. Basically, I have two models: User and Project:
class User(AbstractUser):
username = None
email = models.EmailField("Email Address", unique=True)
avatar = models.ImageField(upload_to="avatars", default="avatars/no_avatar.png")
first_name = models.CharField("First name", max_length=50)
last_name = models.CharField("Last name", max_length=50)
objects = UserManager()
USERNAME_FIELD = "email"
class Project(models.Model):
name = models.CharField("Name", max_length=8, unique=True)
status = models.CharField(
"Status",
max_length=1,
choices=[("O", "Open"), ("C", "Closed")],
default="O",
)
description = models.CharField("Description", max_length=3000, default="")
owner = models.ForeignKey(
User, on_delete=models.SET_NULL, null=True, related_name="project_owner"
)
participants = models.ManyToManyField(User, related_name="project_participants", blank=True)
created_at = models.DateTimeField(auto_now_add=True)
I use standard ModelViewSets for both of them, nothing changed. Then there's my Project serializer:
class ProjectSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = "__all__"
status = serializers.CharField(source="get_status_display", required=False)
owner = UserSerializer()
participants = UserSerializer(many=True)
I use UserSerializers here, because having them achieved first of my two goals:
I wanted to get the user data when getting the project from the API -> owner is a serialized User with all the fields, same for participants, but it's a list of users
I want to be able to partially update the Project, for example add a participant
So I searched through the docs and SO and I always found answers that answer one of those questions, but never both of them.
The thing with my second goal is: when I do the partial update (via PATCH, of course), I get the response that: "Invalid data. Expected a dictionary, but got int." when I pass a list of ints (user ids) for the participants. I thought: okay, maybe I have to pass the whole user data to change it. But then I realised: when I remove the UserSerializer from ProjectSerializer - passing just the list of ints in Postman works just fine. And that is a life saver, cuz who wants to create a request with a whole bunch of data, when I can just pass user ids.
But then of course when I remove the UserSerializer, when I call get project, I get participants: [1,2,3,4,...], not participants: [{"id": 1, "name": "John", ...}, ...}]. And I really want this behavior, because I don't want to make additional API calls just to get the users' data by their IDs.
So summing up my question is: Is there a way to leave those serializers in place but still be able to partially update my model without having to pass whole serialized data to the API (dicts instead of IDs)? Frankly, I don't care about the serializers, so maybe the question is this: Can I somehow make it possible to partially update my Products' related fields like owner or participants just by passing the related entities IDs while still maintaining an ability to get my projects with those fields expanded (serialized entities - dicts, instead of just IDs)?
#Edit:
My view:
from rest_framework import viewsets, permissions
from projects.models import Project
from projects.api.serializers import ProjectSerializer
class ProjectViewSet(viewsets.ModelViewSet):
queryset = Project.objects.all()
serializer_class = ProjectSerializer
permission_classes = [permissions.IsAuthenticated]
lookup_field = "name"
def get_queryset(self):
if self.request.user.is_superuser:
return Project.objects.all()
else:
return Project.objects.filter(owner=self.request.user.id)
def perform_create(self, serializer):
serializer.save(owner=self.request.user, participants=[self.request.user])
Answer:
To anyone reading this, I've solved this problem and I actually created a base class for all my viewsets that I want this behavior to be in:
from rest_framework.response import Response
class ReadWriteViewset:
write_serializer_class = None
read_serializer_class = None
def update(self, request, *args, **kwargs):
partial = kwargs.pop("partial", False)
instance = self.get_object()
write_serializer = self.write_serializer_class(
instance=instance,
data=request.data,
partial=partial,
)
write_serializer.is_valid(raise_exception=True)
self.perform_update(write_serializer)
read_serializer = self.read_serializer_class(instance)
if getattr(instance, "_prefetched_objects_cache", None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
return Response(read_serializer.data)
Then you use it kinda like in here
I'm assuming that you are using a ModelViewSet. You could use different serializers for different methods.
class ProjectViewSet(viewsets.ModelViewSet):
def get_serializer_class(self):
if self.action in ['create', 'update']:
return WriteProjectSerializer # your serializer not using `UserSerializer` that works for updating
return ProjectSerializer # your default serializer with all data
Edit for using different serializers in same method:
# you can override `update` and use a different serializer in the response. The rest of the code is basically the default behavior
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object()
write_serializer = WriteProjectSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
instance = self.perform_update(serializer)
read_serializer = ProjectSerializer(instance)
if getattr(instance, '_prefetched_objects_cache', None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
return Response(read_serializer.data)
A good way to see the default code for all these methods is using Classy DRF. You can see all methods that come with using ModelViewSet and use that code with some changes. Here I'm using the default code for update but changing for a new serializer for the response.

ManyToMany field selects all model instances by default / changing default get_queryset

I changed my .all method so it would select only instances with published=True:
class EventManager(models.Manager):
def all(self, *args, **kwargs):
return super().get_queryset().filter(published=True, *args, **kwargs)
This is related to the problem model fields:
class Event(models.Model):
related_events = models.ManyToManyField('self', blank=True, related_name='related')
published = models.BooleanField(default=False)
objects = EventManager()
As a result ManyToManyField ends up selecting all the Event instances.
What would you suggest me to do in order to save the published functionality and be able to manually add related events? Thank you.
As far as I know, Django does not use Model.objects as manager, but the Model._basemanager, which normally should return all objects.
You can use limit_choices_to [Django-doc] here to limit the choices of the many-to-many field, like:
from django.db.models import Q
class Event(models.Model):
related_events = models.ManyToManyField(
'self',
limit_choices_to=Q(published=True)
blank=False,
related_name='related'
)
published = models.BooleanField(default=False)
objects = EventManager()
You probably also want to remove blank=True, since that means that by default, you make the field not to show op in the forms. So if you want to manually edit the related events, then.blank=False.
Furthermore a ManyToManyField to 'self' is by default symmatrical. This thus means that if event1 is in the related_events of event2, then event2 is in related_events of event1 as well. If you do not want that, you might want to add symmetrical=False [Django-doc].
Note that there are still some scenario's where non-published events might end up in the related events. For example by adding a published event to the related events, and then "unpublish" it.
As for the manager, I think you better patch the get_queryset method:
class EventManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(published=True)
Otherwise there are lot of ways to "circumvent" the filtering. For example: Event.objects.filter(id__gt=-1) would still give me all Events, but since I did not call .all(), this would thus not filter on published.
In the ModelAdmin, you could aim to specify the queryset for this ManyToManyField with:
class EventAdmin(admin.ModelAdmin):
def get_field_queryset(self, db, db_field, request):
if db_field.name == 'event_dates':
return db_field.remote_field.model.base_manager.all()
else:
super(EventAdmin, self).get_field_queryset(db, db_field, request)
That's what I ended up doing in order to show only published events in my html and show all the events (published and unpublished) in admin dashboard.
class EventManager(models.Manager):
"""
changing get_queryset() to show only published events.
if all is set to True it will show both published and unpublished
if False, which is default it will show only published ones
"""
def get_queryset(self, all=False):
if not all:
return super().get_queryset().filter(published=True)
else:
return super().get_queryset()
class Event(models.Model):
related_events = models.ManyToManyField('self', blank=True, related_name='related')
published = models.BooleanField(default=False)
objects = EventManager()
And in ModelAdmin I call get_queryset with all set to True, otherwise I won't be able to see unpublished ones.
class EventAdmin(admin.ModelAdmin):
def get_queryset(self, request):
return Event.objects.get_queryset(all=True)
I could not simply change my model's all method because it would mess with my ManyToManyField by adding all the model instances to to it. So I did all this.

One table can not be foreign key to others except one table

For example
class Room(models.Model):
visitor = models.ForeignKey(Visitor)
number = models.PositiveIntegerField()
capacity = models.ForeignKey(Capacity, on_delete=models.PROTECT)
floor = models.ForeignKey(Floor, on_delete=models.PROTECT)
price = models.PositiveIntegerField()
is_premium = models.BooleanField(default=False)
is_vip = models.BooleanField(default=False)
expiry_date = models.DateTimeField()
class Meta:
unique_together = ('')
def __str__(self):
return '№{0}'.format(self.number)
class Reserved(models.Model):
room = models.ForeignKey(Room)
begin_date = models.DateTimeField()
def __str__(self):
return 'Reserved Room {0}'.format(self.room)
class Busy(models.Model):
room = models.ForeignKey(Room)
Table Room can not be connected to Tables Reserved and Busy at the same time. Room should be reserved or busy. Is there way put validation for this?
I tried to use unique_together but if for fields of table
Thanks
There is no way to enforce this at DB level nor a simple way to do it in Django level. With your structure you should add some validation before creating (or modifying) both Busy and Reserved. Something like:
class Busy(models.Model):
room = models.ForeignKey(Room)
def __save__(self, *args, **kwargs):
if Reserved.object.filter(room=self.room).exists():
raise RuntimeError('Trying to make a reserved room busy.')
super(Busy, self).__save__(*args, **kwargs)
If you are creating Busy and Reserved objects concurrently it's subject to race condition. I suggest to move room state into Room model itself and add some helper functions (something like in room_manager.py beside models.py) to change its state and make sure related models are created/modified in a consistent manner.
The only way to ensure at the database level that you have one single "status" at a given time for a room is to have your relationship the other way round - with Room have a foreign key on whatever represents the status. To make this work you'll need to use either some form of model inheritance or django's "generic" relationship (which can be handy sometimes but are really not SQL-friendly).
Here's an example using the very simplest form of "model inheritance" (which is not actually inheritance at all):
class Status(models.Model):
BUSY = 1
RESERVED = 2
TYPES = (
(BUSY,"busy"),
(RESERVED,"reserved")
)
type = models.CharField("Status type", max_length=10, choices=TYPES)
# only used for reservations
begin_date = models.DateTimeField(null=True, blank=True)
def save(self, *args, **kw):
# TODO : this should belong to `full_clean()`,
# cf the FineManual model's validation
if self.type == self.RESERVED and not self.begin_date:
raise ValueError("RESERVED statuses need a begin_date")
super(Status, self).save(*args, **kw)
class Room(models.Model):
status = models.ForeignKey(Status)
Note that this allows for a same status to be used for multiple rooms at the same time, which might be a problem too. Using a OneToOneField field instead might help on the Django side but will still be treated as a foreign key at the database level.

Django autocategorize in m2m field

I have done a pre_save signal in my django/satchmo inherited model Product called JPiece and I have another model inheritance from satchmo Category called JewelCategory. The pre_save signal makes the JPiece objects get the category list and add those categories that fit the Jpiece description to the relation, that is done in the model, meaning if I manually do
p = Jpiece.objects.get(pk=3)
p.save()
The categories are saved and added to the p.category m2m relation but If i save from the admin it does not do this...
How can I achieve this... to save from the admin a JPiece and to get the categories it belongs too...
Here are the models remember that they both have model inheritance from satchmo product and category classes.
class Pieza(Product):
codacod = models.CharField(_("CODACOD"), max_length=20,
help_text=_("Unique code of the piece. J prefix indicates silver piece, otherwise gold"))
tipocod = models.ForeignKey(Tipo_Pieza, verbose_name=_("Piece Type"),
help_text=_("TIPOCOD"))
tipoenga = models.ForeignKey(Engaste, verbose_name=_("Setting"),
help_text=_("TIPOENGA"))
tipojoya = models.ForeignKey(Estilos, verbose_name=_("Styles"),
help_text=_("TIPOJOYA"))
modelo = models.CharField(_("Model"),max_length=8,
help_text=_("Model No. of casting piece."),
blank=True, null=True)
def autofill(self):
#self.site = Site.objects.get(pk=1)
self.precio = self.unit_price
self.peso_de_piedra = self.stone_weigth
self.cantidades_de_piedra = self.stones_amount
self.for_eda = self.for_eda_pieza
if not self.id:
self.date_added = datetime.date.today()
self.name = str(self.codacod)
self.slug = slugify(self.codacod, instance=self)
cats = []
self.category.clear()
for c in JewelCategory.objects.all():
if not c.parent:
if self.tipocod in c.tipocod_pieza.all():
cats.append(c)
else:
if self.tipocod in c.tipocod_pieza.all() and self.tipojoya in c.estilo.all():
cats.append(c)
self.category.add(*cats)
def pieza_pre_save(sender, **kwargs):
instance = kwargs['instance']
instance.autofill()
# import ipdb;ipdb.set_trace()
pre_save.connect(pieza_pre_save, sender=Pieza)
I know I can be vague with explanations sometimes of what I need so please feel free to ask anything Ill be sure to clarify ASAP since this is a client that needs this urgently.
Thank you all as always...
If you use pre_save, it's called before save(), meaning you can't define m2m relationships since the model doesn't have an ID.
Use post_save.
# this works because the ID does exist
p = Jpiece.objects.get(pk=3)
p.save()
Update, check out the comment here: Django - How to save m2m data via post_save signal?
It looks like the culprit now is that with an admin form, there is a save_m2m() happening AFTER the post_save signal, which could be overwriting your data. Can you exclude the field from the form in your ModelAdmin?
# django.forms.models.py
if commit:
# If we are committing, save the instance and the m2m data immediately.
instance.save()
save_m2m()

Categories

Resources