Django - Symmetrical relation OneToOneField - django

I'm writing and app to manage my network equipments. I created a model, RJ45port, which I can add to my equipment as needed. A RJ45port can be plugged into an other RJ45port and only one.
Here is the model I created :
class RJ45port(models.Model):
plugged_into = models.OneToOneField('self', on_delete=models.SET_NULL, blank=True, null=True)
When I "plug" a RJ45port into another, I want the second one to have "plugged_into" set to the first one. I want the relation to be symmetrical. If I "unplug", I want both of the RJ45 ports to have "plugged_into" set to null, or blank.
I found a bit of code, it might be a hint :
def save(self, *args, **kwargs):
super(RJ45port, self).save()
self.plugged_into.plugged_into = self
To be honest I'm a bit lost here and it's the final step I need to get this app functional...

You are best suited just making a model plug_into() method, and then using it to "plug" one instance into another, as well as an unplug() method.
Example:
class RJ45port(models.Model):
plugged_into = models.OneToOneField('self', on_delete=models.SET_NULL, blank=True, null=True)
def plug_into(self, instance):
self.plugged_into = instance
instance.plugged_into = self
self.save(update_fields=['plugged_into'])
instance.save(update_fields=['plugged_into'])
return [self.plugged_into, instance.plugged_into]
def unplug(self):
self.plugged_into.plugged_into = None
self.plugged_into = None
self.plugged_into.save(update_fields=['plugged_into'])
self.save(update_fields=['plugged_into'])
return [self.plugged_into, instance.plugged_into]
And then you can call it like this:
port_1 = Port.objects.all()[0] # First port
port_2 = Port.objects.all()[1] # Second port
port_1.plug_into(port_2) # Should return [instance, instance]
port_1.unplug() # Should return [None, None]

You are right. Simply override the save method. But call super().save() at the end:
class RJ45port(models.Model):
plugged_into = models.OneToOneField('self', on_delete=models.SET_NULL, blank=True, null=True)
def save(self, *args, **kwargs):
self.plugged_into.plugged_into = self
super(RJ45port, self).save()

Another option is to use a related_name so you can make a reverse access from the referenced instance, so you can say that the relationship becomes "symetrical". The only downside is that you can't use the same name to reference both connections:
class RJ45port(models.Model):
plugged_into = models.OneToOneField('self', on_delete=models.SET_NULL, blank=True, null=True, related_name='plugged_from')
In this example, plugged_from can be queried like any other field from the referenced instance.

Related

prepopulate django formset with certain values

I've struggled for weeks with Django's ModelFormSet & co, head exploding.
I searched hours,days on the internet to find a solution, but nothing really helpful for my special problem:
I have 2 models (here simplified):
class BaseSetting(models.Model):
"""This is a basic model which holds namespace and key of a setting."""
namespace = models.CharField(max_length=25)
key = models.CharField(max_length=255)
class ScopedSetting(models.Model):
"""Model class for all scoped MedUX settings.
Settings are generally saved as strings, but are interpreted at retrieval
and casted into their correct types. ScopedSettings knows about
``str``, ``int``, ``bool``"""
base = models.ForeignKey(BaseSetting, on_delete=models.CASCADE)
"""The FK to the basic settings fields like namespace, key."""
tenant = models.ForeignKey(
Tenant,
verbose_name=_("Tenant"),
on_delete=models.CASCADE,
default=None,
null=True,
blank=True,
)
group = models.ForeignKey(
Group, on_delete=models.CASCADE, default=None, null=True, blank=True
)
device = models.ForeignKey(
Device, on_delete=models.CASCADE, default=None, null=True, blank=True
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
default=None,
null=True,
blank=True,
)
scope = models.IntegerField(choices=Scope.choices)
"""The scope where this setting is valid."""
value = models.CharField(max_length=255, null=True)
ATM I am using django-extra-views to get a simple ModelFormSetView - and it shows a form, but not what I want:
For each BaseSetting (namespace,key) there exists up to 5 different ScopedSettings which could be applicable to the current user, depending on their scope: the vendor, the current tenant, group, device, or user: So each user can have an own setting, each tenant etc., but the user setting for one user, if available as DB object, overrides the vendor's setting. There is a clear permissions system and scope cascade to check which setting is applicable, and all this works. I just want to edit those settings.
So I'd like to have a View under /settings/<base_setting_pk>/ which shows basically (simplified, its even more complicated) all 5 scopes: VENDOR, TENANT, GROUP, DEVICE, USER.
This is the perfect use case for a formset IMHO, BUT: It should show all these 5 Scopedsettings, even when they are not present in the DB.
Here extra=... comes in, and I am able to show the correct number of items.
class BaseSettingsDetailView(HtmxMixin, ModelFormSetView):
# ScopedPermissionRequiredMixin,
model = ScopedSettings
form_class = ScopedSettingsForm
formset = ScopedSettingsModelFormSet
fields = ["value", "scope"]
enforce_htmx = False
ordering = ["scope"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def setup(self, request, *args, **kwargs):
super().setup(request, *args, **kwargs)
self.base_instance = self.get_queryset().first().base
self.scopes = SettingsRegistry.scopes(
self.base_instance.namespace, self.base_instance.key
)
self.additional = []
def get_queryset(self):
return super().get_queryset().filter(base_id=self.kwargs.get("pk"))
def get_factory_kwargs(self):
def formfield_callback(field_name) -> forms.Field:
print(field_name)
if field_name:
return forms.CharField()
for scope in self.scopes:
if self.get_queryset().filter(scope=scope.value).exists():
pass
else:
self.additional.append(
ScopedSettings(base=self.base_instance, scope=scope)
)
factory_kwargs = super().get_factory_kwargs()
max_num = len(self.get_queryset()) + len(self.additional)
factory_kwargs.update(
{"max_num": max_num, "formfield_callback": formfield_callback}
)
return factory_kwargs
But I cant get the Form to show correct data in the empty, uninitialized, extra fields:
They are just labelled "value".
What I need is 5 fields, each labelled by scope. Some of them should be populated with the queryset (this could be the first and the third e.g.), and the others should be empty, but labelled coorectly (and prefilled with the missing scope, each one)
I hope this is understandable.
Any hint welcome.

Checking if a field in model is modified and creating instance of another model

I have two models
Project Model
class Project(models.Model):
name = models.CharField(max_length=200)
workflow = models.ForeignKey("WorkflowType", null=True, blank=True, on_delete=models.SET_NULL)
created_on = models.DateTimeField(auto_now_add=True)
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
def __str__(self):
return self.name
Workflow Instance Model
class WorkflowInstance(models.Model):
workflow_step = models.ForeignKey('WorkflowStep', null=True, blank=True, on_delete=models.CASCADE)
project = models.ForeignKey('Project', null=True, blank=True, on_delete=models.SET_NULL)
I want to check if the value of workflow field in "Project" models is added or changed for a particular project.
I am approaching the problem in following manner:
Checking if the previous and the new value of the "workflow" field in a project are different. If yes (modifies), then create the new instance of a project.
#receiver(pre_save, sender=Project)
def projectToBeUpdated(sender, instance, **kwargs):
if instance.id is None:
pass
else:
previous = Project.objects.get(id=instance.id)
if previous.workflow != instance.workflow:
print("workflow value modified. Please create a WorkflowInstance")
Problem: The comparison for previous and new value of the "workflow" field are happening in "pre_save" signal. But my new instance creation for workflowInstance is to be created in "post_save" signal. How can I do this?
Also, ideally I would like to store the previous value of workflow field in "pre_save" and get the new value of the field in "post_save". Reason being, save() method might fail for any reason, while I am comparing the previous and new value in "pre_save" method itself. Making changes in the database without confirming if the save() method executed successfully would be a wrong approach in my view.
You can override the model save method itself and use the following code logic, additionally if you want to identify what fields got modified you can use the dirtyfields package as well.
def save(self, *args, **kwargs):
if not self._state.adding:
changed_attr = self.get_dirty_fields()
else:
is_new = True
super().save(*args, **kwargs)```

Auto increment django model field per user

I have this model:
class Invoice(models.Model):
owner = models.ForeignKey(settings.AUTH_USER_MODEL)
data = models.TextField(default=None, blank=True, null=True)
number = models.PositiveIntegerField(default=0, null=False)
What I need is to auto-increment the field number for each separated user. The rationale is that each user has a list of Invoice, starting from number=1 to number=latest.number+1.
I do known about F() expressions, but can't figure out how to reference the latest/greatest number for each specific user. Maybe Invoice.objects.filter(owner=request.user).aggregate(Max('number')) is the path, but how do I ensure there is no race conditions between Max() and F()?
You can achieve this and similar functions by overriding save method in model and writing your custom logics to it.
class Invoice(models.Model):
owner = models.ForeignKey(settings.AUTH_USER_MODEL)
data = models.TextField(default=None, blank=True, null=True)
number = models.PositiveIntegerField(default=0, null=False)
def save(self, *args, **kwargs):
if self.pk:
self.number += 1
# Write all your logic here, like handeling max value etc
return super(Invoice, self).save(*args, **kwargs)
you can get your first or last object like this:
# For last Object
Model.objects.latest('field') # Field can be id or pk or ...
# For first Object
Model.objects.all().first() # You can also use it on filter
A simple solution is you can make the number field as the primary key since its nature would be similar.
class Invoice(models.Model):
owner = models.ForeignKey(settings.AUTH_USER_MODEL)
data = models.TextField(default=None, blank=True, null=True)
number = models.IntegerField(primary_key=True)
Or, you can make number as AutoField or BigAutoField.
number = models.AutoField()

Django recursive relationship

I'm using Django 1.5 and am struggling with something that I guess is pretty basic.
I've got following Persons model:
class Person(models.Model):
contact_person = models.ManyToManyField(ContactPerson)
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
...
The goal is to add a partner of the person to the model. I don't want the partner or the person to be superior to the other; they should be equal.
In other words, when looking up a person, his/her partner should appear as well (if there is one). I need to be able to add the partner once, either at the woman's or at the men's side (I don't want to link them two times).
I've searched the documentation for OneToOneFields, but recursive OneToOnes don't seem to be supported, i.e. I get a NameError ("name 'Person' is not defined") when I try:
partner = models.OneToOneField(Person, blank=true, null=true)
Can anyone point me in the right direction?
Try this:
class Person(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
...
partner = models.OneToOneField('self', null=True, blank=True)
def save(self, checkPartner = True, *args, **kwargs):
super(Person, self).save()
if self.partner and checkPartner:
self.partner.partner = self
self.partner.save(checkPartner = False)
Null true on partner field, has to be enabled because the first person to be saved will not have any partner.
checkPartner parameter has been added so that save() doesn't fall into an infinite loop
https://docs.djangoproject.com/en/dev/ref/models/fields/#foreignkey
partner = models.OneToOneField('Person', blank=true, null=true)
or in alternative, if you're on the same class
partner = models.OneToOneField('self', blank=true, null=true)
quotes are everything, in this case
def save(self, *args, **kwargs):
# call to super, we want self.partner to be set
super(Person, self).save(*args, **kwargs)
# this is necessary to avoid infinite save loops on partner's save call
# at this point, you have a partner
# this won't work if your partner has already a partner
# but it's easy to go from here
if not self.partner.partner:
self.partner.partner = self
self.partner.save()

Django Assign M2M after Saving

I'm working on a project in Django and I have the following problem:
I have these two classes, Team and Project. When I create one project I want to automatically assign users from the team what was selected when I created the Project to the new project.
I override Project's save method and after the project was created assign users to the project(I did after saving because, before gave me an error). I tried of several ways but none of them works.
Tried by:
self.user.add(*self.team.users.all())
self.save()
And this doesn't work.
Tried iterating:
for uTeam in self.team.users.all():
self.users.add(uTeam)
and doesn't work either.
The only way that work for me is this, but only in the Django Shell:
P = Project.objects.get(pk=1)
T = Team.objects.get(pk=1)
P.user.add(*T.user.all())
P.save()
This is the solution that I have below but doesn't work in Django(gives an infinite loop)
class Team(models.Model):
name = models.CharField(max_length=200,
help_text=_('name of the team'))
user = models.ManyToManyField(settings.AUTH_USER_MODEL,
related_name="members_of_team",
help_text=_('users of the team'),
null=True
)
and more....
class Project(models.Model):
id = models.AutoField(primary_key=True, null=False)
name = models.CharField(max_length=200,
help_text=_('name of project'),
)
team = models.ForeignKey(Team,`enter code here`
on_delete=models.PROTECT,
help_text=_('team of project'))
created_by = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
related_name='creator_project',
blank=True,
null=False,
help_text=_('project created by'))
customer = models.ForeignKey(Customer,
on_delete=models.PROTECT,
help_text=_('customer'))
user = models.ManyToManyField(settings.AUTH_USER_MODEL,
related_name='users_team',
blank=True,
null=False,
help_text=_('users of this project'))
def save(self, *args, **kwargs):
if self.checkIntegrity():
super(Project, self).save(*args, **kwargs)
if self.user.all().count() < self.team.user.all().count():
T = Team.objects.get(pk=self.team.id)
P = Project.objects.get(pk=self.id)
P.user.add(*T.user.all())
P.save()
Thank you for your help
I got it, I read that m2m fields are filled after save() and post_save() and there is a signal that trigger when a m2m field is changed so I write the following:
#receiver(m2m_changed, sender=Project.user.through)
def m2mChange(sender, **kwargs):
instance = kwargs['instance']
T = Team.objects.get(pk=instance.team.id)
if kwargs['pk_set'] is None:
instance.user.add(*T.user.all())
And now it works fine.
Thank you for all.