Use custom `Manager` for "default values" creation and fetching - python

I have and Employee model and an EmployeeType model, with Employee having an attribute called Employee.employee_type which is of type EmployeeType.
Currently on the creation of a client, we run something to create "default" values for the EmployeeType. Right now, that logic is in a method within the module that handles a new client being created... but I was thinking a better place for this would be to be with the EmployeeType model.
My question is - would it be appropriate to make a custom Manager attribute on the EmployeeType that does the creation and/or fetching of these default types? See the following for what I believe I am trying to accomplish:
class DefaultValueEmployeeTypeManager(models.Manager):
def get_or_create_default_values(self):
first_type = self.model.objects.get_or_create(name='First Type')
second_type = self.model.objects.get_or_create(name='Second Type')
third_type = self.model.objects.get_or_create(name='Third Type')
return (first_type, second_type, third_type)
class Employee(models.Model):
first_name = models.CharField(max_length=255)
last_name = models.CharField(max_length=255)
defaults = DefaultValueEmployeeTypeManager()
# Code in another file, handling the setup of a new client
from models import EmployeeType
def create_new_client(client):
# make sure the default values are there
EmployeeType.defaults.get_or_create_default_values()
My question is whether or not this is acceptable/expected behavior for a Manager object to handle? Or should this just be some sort of #classmethod (or similar) on the EmployeeType model?

You can do that, but I wouldn't rename the manager to defaults because now objects is undefined and all queries on EmployeeType need to use defaults, e.g. EmployeeType.defaults.all() which doesn't really make sense.
You just want to add an extra method, like described here.
Just name your manager objects = EmployeeTypeManager(), call your manager EmployeeTypeManager (it's still the default manager) and your method is just an extra manager method.
Note that get_or_create returns a two-tuple of the object and whether or not it was created. So in your code first_type is (<the object>, False) if the type already exists.
Note also you put this under the Employee model, but it's of course an EmployeeType model manager.

Related

Update a field of a Django Object

I'm trying to update an object field in django. Usually I would do something like this:
# MODEL ---
class MyObj(models.model):
name: models.CharField(max_length=10)
surname: models.CharField(max_length=10)
# VIEW ---
# [...]
myObj = MyObj.objects.get(pk=1)
myObj.name = 'John'
myObj.save()
The problem is that the field to modify (in the example above: "name") is not known and passed as an argument to the post request. So I would have something like this:
# VIEW ---
# [...]
field = self.request.query_params['field_to_modify']
myObj = MyObj.objects.get(pk=1)
myObj[field] = 'John'
myObj.save()
now this triggers the error:
myObj[field] = 'John'
TypeError: 'MyObj' object does not support item assignment
What is the correct way to update an "unknown" field of a django object?
UPDATE
Thank you for your answers so far! OK so the way to go is apparently using setattr (as per the answers). Now the problem is that it does not allow me to save the modified object without checking if it is valid.
So I tried using the Serializer to check the object validity but is not working:
field = self.request.query_params['field_to_modify']
myObj = MyObj.objects.get(pk=1)
setattr(myObj, field, 'John')
serial = MyObjSerializer(myObj)
serial.is_valid(raise_exception=True)
serial.save()
error:
AssertionError: Cannot call `.is_valid()` as no `data=` keyword argument was passed when instantiating the serializer instance.
You're looking for setattr to set an attribute by name.
field = self.request.query_params['field_to_modify']
# TODO: add validation for `field`
myObj = MyObj.objects.get(pk=1)
setattr(myObj, field, 'John')
myObj.save(update_fields=[field]) # no need to update anything but the single field
Of course, this won't let you automatically attach any arbitrary data to models and expect it to be saved; if you need something like that, maybe use a JSON field.
EDIT:
For using a Django REST Framework serializer (which, cough, wasn't part of the original question or tags, but could be inferred from query_params), one could do
field = self.request.query_params['field_to_modify']
myObj = MyObj.objects.get(pk=1)
serial = MyObjSerializer(instance=myObj, data={field: 'John'})
serial.is_valid(raise_exception=True)
serial.save()
Yeah it looks like Django model instances do not support that operation. It looks like you can use setattr though from looking at this previous question / answer. Update model instance with dynamic field names

How to let DjangoModelFactory create a model without saving it to the DB?

I'm writing unit tests in a Django project. I've got a factory to create objects using the .create() method. So in my unit tests I'm using this:
device = DeviceFactory.create()
This always creates a record in the DB though. Is there a way that I can make the factory create an object without saving it to the DB yet?
I looked over the documentation but I can't find it. Am I missing something?
Quoth this bit of the documentation, use .build() instead of .create():
# Returns a User instance that's not saved
user = UserFactory.build()
# Returns a saved User instance.
# UserFactory must subclass an ORM base class, such as DjangoModelFactory.
user = UserFactory.create()
# Returns a stub object (just a bunch of attributes)
obj = UserFactory.stub()
# You can use the Factory class as a shortcut for the default build strategy:
# Same as UserFactory.create()
user = UserFactory()

How to force a Django model to reload fields after saving without using refresh_from_db?

I have the following Django models:
Device had a foreign key to Deployment
I have the adopt method in Device that set the deployment for a device and perform some additional logic.
notice that I pass an id and not a Deployment instance to the adopt method
class Device(Model):
id = models.UUIDField(primary_key=True)
deployment = models.ForeignKey(
Deployment, on_delete=models.SET_NULL, blank=True, null=True, related_name='devices'
)
def adopt(self, deployment_id):
self.deployment_id = deployment_id
self.full_clean()
with transaction.atomic():
self.save()
# self.refresh_from_db()
if self.deployment.private: # this might fail.. see examples
# additional logic
pass
class Deployment(TimeStampedModel, DeploymentResource):
id = models.UUIDField(primary_key=True)
name = models.CharField(max_length=150)
private = models.BooleanField(default=False)
If I do not access to the deployment field of the Device instance, adopt() works as expected.
example:
device = Device.objects.get(pk=device_id)
device.adopt(deployment_id) # this works
if instead I load the deployment field before calling adopt, when I call Adopt I get an AttributeError: 'NoneType' object has no attribute 'private'
this does not works:
device = Device.objects.get(pk=device_id)
print(device.deployment) # example of accessing/loading the field/relation
device.adopt(deployment_id) # this will generate the attribute error
The cause is pretty obvious. device.deployment was None, its value has been loaded and stored inside the model but it's not automatically reloaded and substituted with the new deployment after the save() call.
The first example works simply because the deployment relation has never been accessed before the save.
An obvious solution is to call refresh_from_db after saving (see comment inside adopt method) but this will generate an additional query that I would like to avoid.
Is there a way to force the model to forget the "cached" value of the deployment attribute that does not involve an additional query?
edit: clarified where the exception is raised
I´m not sure if i understand this correctly but to me it seems like the problem is rather that the default value of deployment is null.
So if you try to access deployment without assigning a deployment object first it will be null.
Please check if you are assigning deployment before calling device.deployment and if so please provide some code so i can recreate your situation.

How to filter through Model of a many-to-many field?

I'm trying to implement a geofencing for a fleet of trucks. I have to associate a list of boundaries to a vehicle. On top of that one of the requirements is keep everything even once it is deleted for audit purposes. Therefore we have to implement soft delete on everything. This is where the problem lies. My many to many field does not conform to the soft delete manager, it includes both the active and the inactive records in the lookup dataset.
class Vehicle(SoftDeleteModel):
routes = models.ManyToManyField('RouteBoundary', through='VehicleBoundaryMap', verbose_name=_('routes'),
limit_choices_to={'active': True})
class VehicleBoundaryMap(SoftDeleteModel):
vehicle = models.ForeignKey(Vehicle, verbose_name="vehicle")
route_boundary = models.ForeignKey(RouteBoundary, verbose_name="route boundary")
# ... more stuff here
alive = SoftDeleteManager()
class SoftDeleteManager(models.Manager):
use_for_related_fields = True
def get_queryset(self):
return SoftDeleteQuerySet(self.model).filter(active=True)
As you see above I tried to make sure the default manager is a soft delete manager (ie. filter for active records only) and also try use limit limit_choices_to but that turn out to field the foreign model only not the "through" model I wanted. If you have any suggestions or recommendation I would love to hear from you.
Thanks!
First problem: your use of limit_choices_to won't work because as the documentation says:
limit_choices_to has no effect when used on a ManyToManyField with a custom intermediate table specified using the through parameter.
You are using through so limit_choices_to has no effect.
Second problem: your use of use_for_related_fields = True is also ineffective. The documentation says about this attribute:
If this attribute is set on the default manager for a model (only the default manager is considered in these situations), Django will use that class whenever it needs to automatically create a manager for the class.
Your custom manager is assigned to the alive attribute of VehicleBoundaryMap rather than objects so it is ignored.
The one way I see which may work would be:
Create a proxy model for VehicleBoundaryMap. Let's call it VehicleBoundaryMapProxy. Set it so that its default manager is SoftDeleteManager() Something like:
class VehicleBoundaryMapProxy(VehicleBoundaryMap):
class Meta:
proxy = True
objects = SoftDeleteManager()
Have through='VehicleBounddaryMapProxy' on your ManyToManyField:
class Vehicle(SoftDeleteModel):
routes = models.ManyToManyField('RouteBoundary',
through='VehicleBoundaryMapProxy',
verbose_name=_('routes'))
What about if you just do:
class Vehicle(SoftDeleteModel):
#you can even remove that field
#routes = models.ManyToManyField('RouteBoundary', through='VehicleBoundaryMap', verbose_name=_('routes'),
# limit_choices_to={'active': True})
#property
def routes(self):
return RouteBoundary.objects.filter(
vehicleboundarymap__active=True,
vehicleboundarymap__vehicle=self,
)
And now instead of vehicle.routes.clear() use vehicle.vehicleboundarymap_set.delete(). You will only lose the reverse relation (RouteBoundary.vehicles) but you can implement it back using the same fashion.
The rest of the M2M field features are disabled anyway because of the intermediate model.

Django get_or_create does not return a usable Model object in clean method of ModelForm

Hello,
I have bound a ModelForm to one of my model that contains a ForeignKey to another model everything driven by a CreateView. What I want to achieve is to create the model object corresponding to the foreign key if it doesn't exist before the form is overall validated and the final object created in database.
Below the models I use:
class UmsAlerting(models.Model):
alert_id = models.IntegerField(primary_key=True, editable=False)
appli = models.ForeignKey('UmsApplication')
env = models.ForeignKey('UmsEnvironment')
contact = models.ForeignKey('UmsContacts')
custom_rule = models.ForeignKey('UmsCustomRules', null=True, blank=True)
class UmsApplication(models.Model):
appli_id = models.IntegerField(primary_key=True)
trigram_ums = models.CharField(max_length=4L)
class UmsContacts(models.Model):
contact_id = models.IntegerField(primary_key=True)
mail_addr = models.CharField(max_length=100L)
class UmsEnvironment(models.Model):
env_id = models.IntegerField(primary_key=True)
env_name = models.CharField(max_length=5L)
The model bound to the form is UmsAlerting. The model object I want to create if it doesn't exist is UmsContacts. I managed to use the field's clean method in my ModelForm of the contact field and use the get_or_create method like below:
def clean_contact(self):
data = self.cleaned_data['contact']
c, _ = UmsContacts.objects.get_or_create(mail_addr=data)
return c
It perfectly works when the contact is already in the database but when it needs to be created my form return a ValidationError on the contact field saying "This field cannot be null". If I submit the same form a second time without changing anything the UmsAlerting object is well created with no validation error.
My guess is that, for a reason I don't get, when get_or_create is used to create a UmsContacts object it cannot be used to create the new UmsAlerting object. So in clean_contact method the get is working and returns the UmsContacts object but the create part doesn't. It'd be like the UmsContacts object is saved when the whole form is validated but not before as I'd want it to.
Anyone could help me find out what is the problem ? Is using the clean method not the best idea ? Is there another strategy to use to take around this problem ?
Thanks in advance for your help.
It's probably because the object you are creating expects value for contact_id. If you use contact_id field for just setting object id -then you do not have to create it at all. Django takes care of Id's automatically.
Also. field clean method should return cleaned data not object. That creates whole lot more problems on its own.

Categories