Django recursive relationship - django

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()

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.

Django filter two levels of DB relationships

I have three models that I'm trying to hook up so I can pull-out StudentItem information based upon ID of the Course that was passed in.
The models look like the following:
class Student(models.Model):
first_name = models.CharField(max_length=128, unique=False)
last_name = models.CharField(max_length=128, unique=False)
class Course(models.Model):
name = models.CharField(max_length=256, unique=False)
course_student = models.ManyToManyField(Student)
class StudentItem(models.Model):
item_student = models.ForeignKey('Student',on_delete=models.CASCADE)
description = models.CharField(max_length=256, unique=False, blank=True)
Right now, my view looks like:
class CourseDetailView(LoginRequiredMixin,DetailView):
model = models.Course
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['student_item_list'] = StudentItem.objects.prefetch_related(Prefetch('item_student__id__course_student',queryset=Course.objects.filter(pk=self.kwargs['pk'])))
return context
I can get everything connected via prefetch across all of the models but the queryset filtering is not working. I am getting all records for every course regardless of the course ID that was passed in.
Hopefully it's a small tweak. Really appreciate the help!

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 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.

Django counting related objects in model layer

It is possible to do something like this working:
class Book(models.Model):
voters = models.ManyToManyField(User, blank=True)
vote = models.IntegerField() # summary of all votes
def average_vote(self):
return int(vote/self.annotate(Count('voters')))
Maybe something like this?
class Book(models.Model):
voters = models.ManyToManyField(User, blank=True)
vote = models.IntegerField() # summary of all votes
def average_vote(self):
return int(self.vote/self.voters.all().count())
Let me know if that works. I haven't tested it.
Just override the default manager to make it always return an annotated queryset:
class BookUserManager(models.Manager):
def get_query_set(self, *args, **kwargs):
return super(BookUserManager, self).get_query_set(*args, **kwargs).annotate(average_vote=models.Avg('books__vote'))
class BookUser(User):
objects = BookUserManager()
class Meta:
proxy = True
class Book(models.Model):
# Next line has been changed to use proxy model. This *will* affect the m2m table name.
voters = models.ManyToManyField(BookUser, blank=True, related_name='books')
vote = models.IntegerField() # summary of all votes
objects = BookManager()
Then, you can get at the value like any other attribute on your the user model:
user = BookUser.objects.get(username='joe')
print user.average_vote
Update: Sorry... got that all wrong. That's what I get for reading the question too quickly. You'd actually need to annotate User not Book, but since User is coming from django.contrib.auth (I'm assuming) that's not going to be possible, or at least it requires more steps. Code above has been updated.